Skip to content

Commit

Permalink
feat: add 'oneOf' schema (#523)
Browse files Browse the repository at this point in the history
  • Loading branch information
nicoecheza authored Apr 14, 2023
1 parent 108fa71 commit f6a337a
Show file tree
Hide file tree
Showing 22 changed files with 222 additions and 83 deletions.
1 change: 1 addition & 0 deletions packages/@dcl/ecs/src/schemas/ISchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export type JsonSchemaExtended = {
| 'utf8-string'
| 'protocol-buffer'
| 'transform'
| 'one-of'
| 'unknown'
} & JsonMap

Expand Down
44 changes: 44 additions & 0 deletions packages/@dcl/ecs/src/schemas/OneOf.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { DeepReadonly } from '../engine/readonly'
import { ByteBuffer } from '../serialization/ByteBuffer'
import { ISchema } from './ISchema'
import { Spec } from './Map'

type OneOfType<T extends Spec> = {
[K in keyof T]: {
readonly $case: K
readonly value: ReturnType<T[K]['deserialize']>
}
}[keyof T]

export const IOneOf = <T extends Spec>(specs: T): ISchema<OneOfType<T>> => {
const specKeys = Object.keys(specs)
const keyToIndex = specKeys.reduce((dict: Record<string, number>, key, index) => {
dict[key] = index
return dict
}, {})
const specReflection = specKeys.reduce((specReflection, currentKey) => {
specReflection[currentKey] = specs[currentKey].jsonSchema
return specReflection
}, {} as Record<string, any>)

return {
serialize({ $case, value }: DeepReadonly<OneOfType<T>>, builder: ByteBuffer): void {
const _value = keyToIndex[$case.toString()] + 1
builder.writeUint8(_value)
;(specs as any)[$case].serialize(value, builder)
},
deserialize(reader: ByteBuffer) {
const $case = specKeys[reader.readInt8() - 1]
const value = specs[$case].deserialize(reader)
return { $case, value }
},
create() {
return {} as OneOfType<T>
},
jsonSchema: {
type: 'object',
properties: specReflection,
serializationType: 'one-of'
}
}
}
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
import { IArray } from './Array'
import { Bool } from './basic/Boolean'
import { IntEnum, StringEnum } from './basic/Enum'
import { Float32, Float64 } from './basic/Float'
import { Int16, Int32, Int64, Int8 } from './basic/Integer'
import { EcsString } from './basic/String'
import { Color3Schema } from './custom/Color3'
import { Color4Schema } from './custom/Color4'
import { EntitySchema } from './custom/Entity'
import { QuaternionSchema } from './custom/Quaternion'
import { Vector3Schema } from './custom/Vector3'
import { ISchema, JsonSchemaExtended } from './ISchema'
import { IMap } from './Map'
import { IOptional } from './Optional'
import { IArray } from '../Array'
import { Bool } from '../basic/Boolean'
import { IntEnum, StringEnum } from '../basic/Enum'
import { Float32, Float64 } from '../basic/Float'
import { Int16, Int32, Int64, Int8 } from '../basic/Integer'
import { EcsString } from '../basic/String'
import { Color3Schema } from '../custom/Color3'
import { Color4Schema } from '../custom/Color4'
import { EntitySchema } from '../custom/Entity'
import { QuaternionSchema } from '../custom/Quaternion'
import { Vector3Schema } from '../custom/Vector3'
import { ISchema, JsonSchemaExtended } from '../ISchema'
import { IMap } from '../Map'
import { IOneOf } from '../OneOf'
import { IOptional } from '../Optional'
import { getTypeAndValue, isCompoundType } from './utils'

const primitiveSchemas = {
[Bool.jsonSchema.serializationType]: Bool,
Expand Down Expand Up @@ -62,11 +64,21 @@ export function jsonSchemaToSchema(jsonSchema: JsonSchemaExtended): ISchema<any>
const enumJsonSchema = jsonSchema as JsonSchemaExtended & { enumObject: Record<any, any>; default: number }
return IntEnum(enumJsonSchema.enumObject, enumJsonSchema.default)
}

if (jsonSchema.serializationType === 'enum-string') {
const enumJsonSchema = jsonSchema as JsonSchemaExtended & { enumObject: Record<any, any>; default: string }
return StringEnum(enumJsonSchema.enumObject, enumJsonSchema.default)
}

if (jsonSchema.serializationType === 'one-of') {
const oneOfJsonSchema = jsonSchema as JsonSchemaExtended & { properties: Record<string, JsonSchemaExtended> }
const spec: Record<string, ISchema> = {}
for (const key in oneOfJsonSchema.properties) {
spec[key] = jsonSchemaToSchema(oneOfJsonSchema.properties[key])
}
return IOneOf(spec)
}

throw new Error(`${jsonSchema.serializationType} is not supported as reverse schema generation.`)
}

Expand All @@ -76,31 +88,31 @@ export function mutateValues(
mutateFn: (value: unknown, valueType: JsonSchemaExtended) => { changed: boolean; value?: any }
): void {
if (jsonSchema.serializationType === 'map') {
const mapJsonSchema = jsonSchema as JsonSchemaExtended & { properties: Record<string, JsonSchemaExtended> }
const mapValue = value as Record<string, unknown>
const { properties } = jsonSchema as JsonSchemaExtended & { properties: Record<string, JsonSchemaExtended> }
const typedValue = value as Record<string, unknown>

for (const key in mapJsonSchema.properties) {
const valueType = mapJsonSchema.properties[key]
if (valueType.serializationType === 'array' || valueType.serializationType === 'map') {
mutateValues(mapJsonSchema.properties[key], mapValue[key], mutateFn)
for (const key in properties) {
const { type, value: mapValue } = getTypeAndValue(properties, typedValue, key)
if (type.serializationType === 'unknown') continue
if (isCompoundType(type)) {
mutateValues(type, mapValue, mutateFn)
} else {
const newValue = mutateFn(mapValue[key], valueType)
const newValue = mutateFn(mapValue, type)
if (newValue.changed) {
mapValue[key] = newValue.value
typedValue[key] = newValue.value
}
}
}
} else if (jsonSchema.serializationType === 'array') {
const withItemsJsonSchema = jsonSchema as JsonSchemaExtended & { items: JsonSchemaExtended }
const { items } = jsonSchema as JsonSchemaExtended & { items: JsonSchemaExtended }
const arrayValue = value as unknown[]
const nestedMutateValues =
withItemsJsonSchema.items.serializationType === 'array' || withItemsJsonSchema.items.serializationType === 'map'

for (let i = 0, n = arrayValue.length; i < n; i++) {
if (nestedMutateValues) {
mutateValues(withItemsJsonSchema.items, arrayValue[i], mutateFn)
const { type, value } = getTypeAndValue({ items: items }, { items: arrayValue[i] }, 'items')
if (isCompoundType(type)) {
mutateValues(type, value, mutateFn)
} else {
const newValue = mutateFn(arrayValue[i], withItemsJsonSchema.items)
const newValue = mutateFn(value, type)
if (newValue.changed) {
arrayValue[i] = newValue.value
}
Expand Down
3 changes: 3 additions & 0 deletions packages/@dcl/ecs/src/schemas/buildSchema/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { JsonSchemaExtended } from '../ISchema'

export type UnknownSchema = { type: JsonSchemaExtended; value: unknown }
40 changes: 40 additions & 0 deletions packages/@dcl/ecs/src/schemas/buildSchema/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { JsonSchemaExtended } from '../ISchema'
import { IOneOf } from '../OneOf'
import { UnknownSchema } from './types'

export const isSchemaType = (value: JsonSchemaExtended, types: JsonSchemaExtended['serializationType'][]) =>
types.includes(value.serializationType)

export const isOneOfJsonSchema = (
type: JsonSchemaExtended
): type is JsonSchemaExtended & { properties: Record<string, JsonSchemaExtended> } => isSchemaType(type, ['one-of'])

export const getUnknownSchema = (): UnknownSchema => ({
type: { type: 'object', serializationType: 'unknown' },
value: undefined
})

export const isCompoundType = (type: JsonSchemaExtended): boolean => isSchemaType(type, ['array', 'map'])

export const getTypeAndValue = (
properties: Record<string, JsonSchemaExtended>,
value: Record<string, unknown>,
key: string
): UnknownSchema => {
const type = properties[key]
const valueKey = value[key]

if (isOneOfJsonSchema(type)) {
const typedMapValue = valueKey as ReturnType<ReturnType<typeof IOneOf>['deserialize']>
if (!typedMapValue.$case) return getUnknownSchema()

const propType = type.properties[typedMapValue.$case]

// transform { $case: string; value: unknown } => { [$case]: value }
if (isCompoundType(propType)) value[key] = { [typedMapValue.$case]: typedMapValue.value }

return { type: propType, value: typedMapValue.value }
}

return { type, value: valueKey }
}
3 changes: 3 additions & 0 deletions packages/@dcl/ecs/src/schemas/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { Vector3Schema, Vector3Type } from './custom/Vector3'
import { ISchema, JsonSchemaExtended, JsonArray, JsonMap, JsonPrimitive } from './ISchema'
import { IMap } from './Map'
import { IOptional } from './Optional'
import { IOneOf } from './OneOf'
import { jsonSchemaToSchema, mutateValues } from './buildSchema'

export {
Expand Down Expand Up @@ -78,6 +79,8 @@ export namespace Schemas {
export const Map = IMap
/** @public */
export const Optional = IOptional
/** @public */
export const OneOf = IOneOf

/**
* @public Create an ISchema object from the json-schema
Expand Down
8 changes: 4 additions & 4 deletions packages/@dcl/inspector/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
{
"name": "@dcl/inspector",
"version": "0.1.0",
"dependencies": {
"classnames": "^2.3.2"
},
"devDependencies": {
"@babylonjs/core": "^5.48.0",
"@babylonjs/gui": "^5.48.0",
Expand Down Expand Up @@ -39,8 +42,5 @@
"start": "node ./build.js --watch"
},
"types": "dist/tooling-entrypoint.d.ts",
"typings": "dist/tooling-entrypoint.d.ts",
"dependencies": {
"classnames": "^2.3.2"
}
"typings": "dist/tooling-entrypoint.d.ts"
}
7 changes: 6 additions & 1 deletion packages/@dcl/playground-assets/etc/playground-assets.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -1238,7 +1238,7 @@ export type JsonPrimitive = string | number | boolean | null;
// @public
export type JsonSchemaExtended = {
type: 'object' | 'number' | 'integer' | 'string' | 'array' | 'boolean';
serializationType: 'boolean' | 'enum-int' | 'enum-string' | 'int8' | 'int16' | 'int32' | 'int64' | 'float32' | 'float64' | 'vector3' | 'color3' | 'quaternion' | 'color4' | 'map' | 'optional' | 'entity' | 'array' | 'utf8-string' | 'protocol-buffer' | 'transform' | 'unknown';
serializationType: 'boolean' | 'enum-int' | 'enum-string' | 'int8' | 'int16' | 'int32' | 'int64' | 'float32' | 'float64' | 'vector3' | 'color3' | 'quaternion' | 'color4' | 'map' | 'optional' | 'entity' | 'array' | 'utf8-string' | 'protocol-buffer' | 'transform' | 'one-of' | 'unknown';
} & JsonMap;

// Warning: (tsdoc-undefined-tag) The TSDoc tag "@hidden" is not defined in this configuration
Expand Down Expand Up @@ -2606,6 +2606,11 @@ export namespace Schemas {
Map: <T extends Spec>(spec: T, defaultValue?: Partial<MapResult<T>> | undefined) => ISchema<MapResult<T>>;
const // (undocumented)
Optional: <T>(spec: ISchema<T>) => ISchema<T | undefined>;
const // (undocumented)
OneOf: <T extends Spec>(specs: T) => ISchema<{ [K in keyof T]: {
readonly $case: K;
readonly value: ReturnType<T[K]["deserialize"]>;
}; }[keyof T]>;
const // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
fromJson: (json: JsonSchemaExtended) => ISchema<unknown>;
const mutateNestedValues: (jsonSchema: JsonSchemaExtended, value: unknown, mutateFn: (value: unknown, valueType: JsonSchemaExtended) => {
Expand Down
4 changes: 4 additions & 0 deletions packages/@dcl/sdk-commands/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

39 changes: 33 additions & 6 deletions test/ecs/serialization.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,8 @@ describe('test schema serialization', () => {
hp: Schemas.Float,
position: Vector3,
targets: Schemas.Array(Vector3),
items: Schemas.Array(ItemType)
items: Schemas.Array(ItemType),
pet: Schemas.OneOf({ cat: Schemas.Entity, dog: Schemas.Entity })
})

const defaultPlayer = {
Expand All @@ -119,7 +120,8 @@ describe('test schema serialization', () => {
hp: 0.0,
position: { x: 1.0, y: 50.0, z: 50.0 },
targets: [],
items: []
items: [],
pet: { $case: 'dog' as const, value: 3146 as Entity }
}

const myPlayer = PlayerComponent.create(myEntity, defaultPlayer)
Expand All @@ -143,6 +145,7 @@ describe('test schema serialization', () => {
itemAmount: 10,
description: 'this is a description to an enchanting item.'
})
myPlayer.pet = { $case: 'cat', value: 2019 as Entity }

const buffer = new ReadWriteByteBuffer()
PlayerComponent.schema.serialize(PlayerComponent.get(myEntity), buffer)
Expand Down Expand Up @@ -579,7 +582,8 @@ describe('test json-schema function', () => {

const comp = engine.defineComponent('test', {
arrayOf: Schemas.Array(Schemas.Map(mapWithAllPrimitives)),
mapOf: Schemas.Map(mapWithAllPrimitives)
mapOf: Schemas.Map(mapWithAllPrimitives),
oneOf: Schemas.OneOf(mapWithAllPrimitives)
})

const jsonSchemaComponent = JSON.parse(JSON.stringify(comp.schema.jsonSchema))
Expand Down Expand Up @@ -609,12 +613,27 @@ describe('test json-schema function', () => {
manyEntities: Schemas.Array(Schemas.Entity),
valueWithoutChanges: Schemas.Int,
manyPairOfEntities: Schemas.Array(Schemas.Array(Schemas.Entity))
})
}),
oneOrTheOther: Schemas.OneOf({ someEntity: Schemas.Entity, someBool: Schemas.Boolean }),
oneOrTheOtherMap: Schemas.OneOf({
first: Schemas.Map({ anEntity: Schemas.Entity }),
second: Schemas.Map({ aNumber: Schemas.Number })
}),
oneOrOtherArray: Schemas.Array(Schemas.OneOf({ someEntity: Schemas.Entity, someBool: Schemas.Boolean })),
oneOrTheOtherWithoutChanges: Schemas.OneOf({ someEntity: Schemas.Entity, someBool: Schemas.Boolean }),
nestedOneOrTheOtherWithoutChanges: Schemas.OneOf({
first: Schemas.Map({ anEntity: Schemas.Entity }),
second: Schemas.Map({ aNumber: Schemas.Number })
}),
arrayOfOneOrTheOtherWithoutChanges: Schemas.Array(
Schemas.OneOf({ someEntity: Schemas.Entity, someBool: Schemas.Boolean })
)
}

const MySchema = Schemas.Map(MySchemaDefinition)

const someValue = MySchema.create()

someValue.someImportantEntity = 1 as Entity
someValue.manyEntities = [2, 3, 4] as Entity[]
someValue.manyPairOfEntities = [
Expand All @@ -630,6 +649,9 @@ describe('test json-schema function', () => {
[22, 23, 24, 25]
] as Entity[][]
someValue.nestedMap.valueWithoutChanges = 26
someValue.oneOrTheOther = { $case: 'someEntity', value: 27 as Entity }
someValue.oneOrTheOtherMap = { $case: 'first', value: { anEntity: 28 as Entity } }
someValue.oneOrOtherArray = [{ $case: 'someEntity', value: 29 as Entity }]

mutateValues(MySchema.jsonSchema, someValue, (currentValue, valueType) => {
if (valueType.serializationType === 'entity') {
Expand All @@ -646,7 +668,6 @@ describe('test json-schema function', () => {
[1009, 1010, 1011, 1012]
] as Entity[][],
valueWithoutChanges: 13,

nestedMap: {
someImportantEntity: 1014 as Entity,
manyEntities: [1015, 1016, 1017] as Entity[],
Expand All @@ -655,7 +676,13 @@ describe('test json-schema function', () => {
[1022, 1023, 1024, 1025]
] as Entity[][],
valueWithoutChanges: 26
}
},
oneOrTheOther: 1027 as Entity,
oneOrTheOtherMap: { first: { anEntity: 1028 as Entity } },
oneOrOtherArray: [1029],
oneOrTheOtherWithoutChanges: {},
nestedOneOrTheOtherWithoutChanges: {},
arrayOfOneOrTheOtherWithoutChanges: []
})
})
})
Loading

0 comments on commit f6a337a

Please sign in to comment.