All files / packages/value/src struct.ts

96.42% Statements 54/56
82.6% Branches 19/23
100% Functions 11/11
96% Lines 48/50

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130                  50x 50x       50x 2x 2x   2x     170x 159x   48x 170x 170x         130x       41x 41x 41x 156x           41x         41x       112x 427x                 114x       114x 112x   112x 427x   427x           427x 1x         426x 6x   420x       111x     2x 2x 2x     4x 2x   2x 4x 4x           4x 4x 4x     2x       106x 106x 415x       106x      
import { create } from '@bufbuild/protobuf'
import * as Ydb from '@ydbjs/api/value'
 
import { Optional, OptionalType } from './optional.js'
import { typeToString } from './print.js'
import { type Type, TypeKind } from './type.js'
import type { Value } from './value.js'
 
export class StructType implements Type {
	readonly names: string[] = []
	readonly types: Type[] = []
	#typeInstance?: Ydb.Type
 
	constructor(names: string[], types: Type[], sorted = false) {
		if (sorted) {
			this.names = names
			this.types = types
 
			return
		}
 
		const indices = names.map((_, i) => i)
		indices.sort((a, b) => names[a]!.localeCompare(names[b]!))
 
		for (let i of indices) {
			this.names.push(names[i]!)
			this.types.push(types[i]!)
		}
	}
 
	get kind(): TypeKind.STRUCT {
		return TypeKind.STRUCT
	}
 
	encode(): Ydb.Type {
		Eif (!this.#typeInstance) {
			let members: { name: string; type: Ydb.Type }[] = []
			for (let i = 0; i < this.names.length; i++) {
				members.push({
					name: this.names[i]!,
					type: this.types[i]!.encode(),
				})
			}
 
			this.#typeInstance = create(Ydb.TypeSchema, {
				type: { case: 'structType', value: { members } },
			})
		}
 
		return this.#typeInstance
	}
 
	*[Symbol.iterator](): Iterator<[string, Type]> {
		for (let i = 0; i < this.names.length; i++) {
			yield [this.names[i]!, this.types[i]!]
		}
	}
}
 
export class Struct<
	T extends Record<string, Value | null> = Record<string, Value>,
> implements Value<StructType> {
	readonly type: StructType
	readonly items: Value[] = []
	#valueInstance?: Ydb.Value
 
	constructor(obj: T, def?: StructType) {
		if (def) {
			this.type = def
 
			for (let [name, type] of def) {
				let objValue = obj[name] ?? null
 
				Iif (objValue && objValue.type.kind !== type.kind) {
					throw new Error(
						`Invalid type for ${name}: expected ${typeToString(type)}, got ${typeToString(objValue.type)}`
					)
				}
 
				if (objValue === null && type.kind !== TypeKind.OPTIONAL) {
					throw new Error(
						`Field ${name} is declared as ${typeToString(type)} but no value provided.`
					)
				}
 
				if (objValue === null) {
					this.items.push(new Optional(null, (type as OptionalType).itemType))
				} else {
					this.items.push(objValue)
				}
			}
 
			return
		}
 
		let keys = Object.keys(obj)
		let names: string[] = []
		let types: Type[] = []
 
		// Sort both arrays based on names
		const indices = keys.map((_, i) => i)
		indices.sort((a, b) => keys[a]!.localeCompare(keys[b]!))
 
		for (let i of indices) {
			let key = keys[i]!
			Iif (obj[key] === null) {
				throw new Error(
					`Invalid value for ${key}: expected a value, got null. Please provide a type definition.`
				)
			}
 
			names.push(key)
			types.push(obj[key]!.type)
			this.items.push(obj[key]!)
		}
 
		this.type = new StructType(names, types, true)
	}
 
	encode(): Ydb.Value {
		Eif (!this.#valueInstance) {
			this.#valueInstance = create(Ydb.ValueSchema, {
				items: this.items.map((i) => i.encode()),
			})
		}
 
		return this.#valueInstance
	}
}