From b7195d4d4e658d9a7c9c573a4396cd172d886416 Mon Sep 17 00:00:00 2001 From: Ali Date: Sun, 31 May 2026 14:27:29 +0200 Subject: [PATCH 1/2] Introduce MCollection, MArray, and MOne implementation in javascript compiler --- .gitignore | 1 + .../content/docs/js/emi-array-data-type.mdx | 158 ++++-- .../content/docs/js/emi-boolean-data-type.mdx | 16 - .../src/content/docs/js/emi-complex-types.mdx | 8 - .../emi-web/src/content/docs/js/emi-enum.md | 4 +- .../content/docs/js/emi-fields-auto-init.mdx | 64 ++- .../content/docs/js/emi-float64-data-type.mdx | 16 - .../src/content/docs/js/emi-int-data-type.mdx | 16 - .../docs/js/emi-javascript-web-socket.mdx | 8 - .../content/docs/js/emi-nullable-object.md | 39 +- .../content/docs/js/emi-object-data-type.mdx | 16 - .../src/content/docs/js/emi-object.mdx | 8 - .../src/content/docs/js/emi-static-fields.mdx | 40 +- .../content/docs/js/emi-string-data-type.mdx | 16 - examples/js-sdk-kit/build/common/operators.js | 54 +- examples/js-sdk-kit/src/common/operators.ts | 67 ++- .../reactclient/src/generated/AverageDto.ts | 8 - .../reactclient/src/generated/ComputeDto.ts | 8 - .../src/generated/GetSinglePostAction.ts | 8 - .../src/generated/SampleSseAction.ts | 8 - .../src/generated/WebSocketOrgEchoAction.ts | 60 +- .../src/generated/sdk/common/operators.ts | 67 ++- examples/js/cases/User.ts | 92 ++++ examples/js/cases/emi-data-type.output.ts | 1 - ...-js-array-one-collection-wrapper.output.js | 431 +++++++++++++++ ...-js-array-one-collection-wrapper.output.ts | 517 ++++++++++++++++++ ...mi-js-array-one-collection-wrapper.test.ts | 329 +++++++++++ examples/js/cases/emi-module-enums.test.ts | 4 +- .../js/cases/emi-object-auto-init.test.ts | 16 +- examples/js/cases/scenarios.output.js | 431 +++++++++++++++ .../js/cases/sdk/common/URLSearchParamsX.ts | 144 +++++ examples/js/cases/sdk/common/WebSocketX.ts | 97 ++++ examples/js/cases/sdk/common/buildUrl.ts | 34 ++ examples/js/cases/sdk/common/fetchx.ts | 196 +++++++ .../js/cases/sdk/common/isPlausibleObject.ts | 18 + examples/js/cases/sdk/common/operators.ts | 211 +++++++ examples/js/cases/sdk/common/withPrefix.ts | 22 + .../js/cases/sdk/js/is-plausible-object.ts | 25 + examples/js/cases/sdk/react/useFetchx.ts | 10 + examples/js/cases/sdk/react/useSse.ts | 68 +++ examples/js/cases/sdk/react/useWebSocketX.ts | 104 ++++ examples/js/common.ts | 19 +- examples/js/test-artifacts/auto-init.dto.ts | 64 ++- .../HttpActionAction.ts | 8 - .../sdk/common/operators.ts | 67 ++- .../http-emi-react-output/HttpActionAction.ts | 8 - .../sdk/common/operators.ts | 67 ++- .../HttpActionAction.ts | 8 - .../sdk/common/operators.ts | 67 ++- .../js/test-artifacts/sdk/common/operators.ts | 38 ++ .../UserStreamAction.ts | 8 - .../sdk/common/operators.ts | 67 ++- examples/js/vite.config.ts | 15 + lib/core/common.go | 29 + lib/js/js-common-fields.go | 14 +- lib/js/js-common-object-class.go | 12 +- lib/js/js-common-object.go | 46 +- lib/js/js-sdk/common/operators.js | 54 +- lib/js/js-setter-function.go | 133 ++++- lib/js/ts-sdk/common/operators.ts | 67 ++- 60 files changed, 3766 insertions(+), 465 deletions(-) create mode 100644 examples/js/cases/User.ts create mode 100644 examples/js/cases/emi-js-array-one-collection-wrapper.output.js create mode 100644 examples/js/cases/emi-js-array-one-collection-wrapper.output.ts create mode 100644 examples/js/cases/emi-js-array-one-collection-wrapper.test.ts create mode 100644 examples/js/cases/scenarios.output.js create mode 100644 examples/js/cases/sdk/common/URLSearchParamsX.ts create mode 100644 examples/js/cases/sdk/common/WebSocketX.ts create mode 100644 examples/js/cases/sdk/common/buildUrl.ts create mode 100644 examples/js/cases/sdk/common/fetchx.ts create mode 100644 examples/js/cases/sdk/common/isPlausibleObject.ts create mode 100644 examples/js/cases/sdk/common/operators.ts create mode 100644 examples/js/cases/sdk/common/withPrefix.ts create mode 100644 examples/js/cases/sdk/js/is-plausible-object.ts create mode 100644 examples/js/cases/sdk/react/useFetchx.ts create mode 100644 examples/js/cases/sdk/react/useSse.ts create mode 100644 examples/js/cases/sdk/react/useWebSocketX.ts diff --git a/.gitignore b/.gitignore index 349ba91a..5f5b9a0e 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,4 @@ emi.exe examples/in-browser-server/browser/server examples/emi-wasm-helper/emi-compiler.wasm playground/public/emi-compiler.wasm +examples/js/last-failed-gen diff --git a/examples/emi-web/src/content/docs/js/emi-array-data-type.mdx b/examples/emi-web/src/content/docs/js/emi-array-data-type.mdx index e5fa24a9..bd1e42bf 100644 --- a/examples/emi-web/src/content/docs/js/emi-array-data-type.mdx +++ b/examples/emi-web/src/content/docs/js/emi-array-data-type.mdx @@ -25,14 +25,7 @@ fields: - `array?` allows `null`/ `undefined`. ```ts -import { - MArray, - MArrayNullable, - MCollection, - MCollectionNullable, - MOne, - MOneNullable, -} from "./sdk/common/operators"; +import { MArray } from "./sdk/common/operators"; import { type PartialDeep } from "./sdk/common/fetchx"; import { withPrefix } from "./sdk/common/withPrefix"; /** @@ -43,7 +36,9 @@ export class MyArrayClassDto { * * @type {MyArrayClassDto.Contacts} **/ - #contacts: InstanceType[] = []; + #contacts: MArray> = MArray.of( + [], + ); /** * * @returns {MyArrayClassDto.Contacts} @@ -55,17 +50,44 @@ export class MyArrayClassDto { * * @type {MyArrayClassDto.Contacts} **/ - set contacts(value: InstanceType[]) { - if (!Array.isArray(value) && !(value instanceof MCollection)) { + set contacts( + value: + | MArray> + | InstanceType[], + ) { + // When the passed value is already an array, we check if we need to + // cast the inner items into class instance. + if (Array.isArray(value)) { + if (value.length > 0 && value[0] instanceof MyArrayClassDto.Contacts) { + this.#contacts = MArray.of(value); + } else { + this.#contacts = MArray.of( + value.map((item) => new MyArrayClassDto.Contacts(item)), + ); + } return; } - if (value.length > 0 && value[0] instanceof MyArrayClassDto.Contacts) { + // If the instance is already an MArray, we assume it's all good. + if (value instanceof MArray) { this.#contacts = value; - } else { - this.#contacts = value.map((item) => new MyArrayClassDto.Contacts(item)); + return; } + // If the value is not array, and is not a MArray, we need to be consider, + // it might be eligible to be casted into MArray. + const { ok, value: mcastValue } = MArray.cast(value); + if (ok) { + this.#contacts = mcastValue as any; + return; + } + console.warn( + "Cannot assing value to contacts, because it needs MArray instance or an Array.", + ); } - setContacts(value: InstanceType[]) { + setContacts( + value: + | MArray> + | InstanceType[], + ) { this.contacts = value; return this; } @@ -348,14 +370,7 @@ fields: Arrays can hold primitive types or nested objects. ```ts -import { - MArray, - MArrayNullable, - MCollection, - MCollectionNullable, - MOne, - MOneNullable, -} from "./sdk/common/operators"; +import { MArray } from "./sdk/common/operators"; import { type PartialDeep } from "./sdk/common/fetchx"; import { withPrefix } from "./sdk/common/withPrefix"; /** @@ -366,7 +381,9 @@ export class MyArrayClassDto { * * @type {MyArrayClassDto.Contacts} **/ - #contacts: InstanceType[] = []; + #contacts: MArray> = MArray.of( + [], + ); /** * * @returns {MyArrayClassDto.Contacts} @@ -378,17 +395,44 @@ export class MyArrayClassDto { * * @type {MyArrayClassDto.Contacts} **/ - set contacts(value: InstanceType[]) { - if (!Array.isArray(value) && !(value instanceof MCollection)) { + set contacts( + value: + | MArray> + | InstanceType[], + ) { + // When the passed value is already an array, we check if we need to + // cast the inner items into class instance. + if (Array.isArray(value)) { + if (value.length > 0 && value[0] instanceof MyArrayClassDto.Contacts) { + this.#contacts = MArray.of(value); + } else { + this.#contacts = MArray.of( + value.map((item) => new MyArrayClassDto.Contacts(item)), + ); + } return; } - if (value.length > 0 && value[0] instanceof MyArrayClassDto.Contacts) { + // If the instance is already an MArray, we assume it's all good. + if (value instanceof MArray) { this.#contacts = value; - } else { - this.#contacts = value.map((item) => new MyArrayClassDto.Contacts(item)); + return; + } + // If the value is not array, and is not a MArray, we need to be consider, + // it might be eligible to be casted into MArray. + const { ok, value: mcastValue } = MArray.cast(value); + if (ok) { + this.#contacts = mcastValue as any; + return; } + console.warn( + "Cannot assing value to contacts, because it needs MArray instance or an Array.", + ); } - setContacts(value: InstanceType[]) { + setContacts( + value: + | MArray> + | InstanceType[], + ) { this.contacts = value; return this; } @@ -666,14 +710,7 @@ fields: Defaults to `undefined`, but you can assign an array or `null`. ```ts -import { - MArray, - MArrayNullable, - MCollection, - MCollectionNullable, - MOne, - MOneNullable, -} from "./sdk/common/operators"; +import { MArray } from "./sdk/common/operators"; import { type PartialDeep } from "./sdk/common/fetchx"; import { withPrefix } from "./sdk/common/withPrefix"; /** @@ -685,7 +722,7 @@ export class MyArrayClassDto { * @type {MyArrayClassDto.NullableTags} **/ #nullableTags?: - | InstanceType[] + | MArray> | null | undefined | null = undefined; @@ -702,28 +739,55 @@ export class MyArrayClassDto { **/ set nullableTags( value: - | InstanceType[] + | MArray> | null | undefined + | InstanceType[] | null | undefined, ) { - if (!Array.isArray(value) && !(value instanceof MCollection)) { + // For nullable array, we allow explicit undefined or null values + if (value === null || value === undefined) { + this.#nullableTags = value; + return; + } + // When the passed value is already an array, we check if we need to + // cast the inner items into class instance. + if (Array.isArray(value)) { + if ( + value.length > 0 && + value[0] instanceof MyArrayClassDto.NullableTags + ) { + this.#nullableTags = MArray.of(value); + } else { + this.#nullableTags = MArray.of( + value.map((item) => new MyArrayClassDto.NullableTags(item)), + ); + } return; } - if (value.length > 0 && value[0] instanceof MyArrayClassDto.NullableTags) { + // If the instance is already an MArray, we assume it's all good. + if (value instanceof MArray) { this.#nullableTags = value; - } else { - this.#nullableTags = value.map( - (item) => new MyArrayClassDto.NullableTags(item), - ); + return; } + // If the value is not array, and is not a MArray, we need to be consider, + // it might be eligible to be casted into MArray. + const { ok, value: mcastValue } = MArray.cast(value); + if (ok) { + this.#nullableTags = mcastValue as any; + return; + } + console.warn( + "Cannot assing value to nullableTags, because it needs MArray instance or an Array.", + ); } setNullableTags( value: - | InstanceType[] + | MArray> | null | undefined + | InstanceType[] | null | undefined, ) { diff --git a/examples/emi-web/src/content/docs/js/emi-boolean-data-type.mdx b/examples/emi-web/src/content/docs/js/emi-boolean-data-type.mdx index 3d9c59ba..5d96254c 100644 --- a/examples/emi-web/src/content/docs/js/emi-boolean-data-type.mdx +++ b/examples/emi-web/src/content/docs/js/emi-boolean-data-type.mdx @@ -30,14 +30,6 @@ fields: Example with a non-nullable `bool`: ```ts -import { - MArray, - MArrayNullable, - MCollection, - MCollectionNullable, - MOne, - MOneNullable, -} from "./sdk/common/operators"; import { type PartialDeep } from "./sdk/common/fetchx"; /** * The base class definition for myBoolClassDto @@ -219,14 +211,6 @@ fields: ```ts -import { - MArray, - MArrayNullable, - MCollection, - MCollectionNullable, - MOne, - MOneNullable, -} from "./sdk/common/operators"; import { type PartialDeep } from "./sdk/common/fetchx"; /** * The base class definition for myBoolClassDto diff --git a/examples/emi-web/src/content/docs/js/emi-complex-types.mdx b/examples/emi-web/src/content/docs/js/emi-complex-types.mdx index 5e3dc9ab..59720bf1 100644 --- a/examples/emi-web/src/content/docs/js/emi-complex-types.mdx +++ b/examples/emi-web/src/content/docs/js/emi-complex-types.mdx @@ -40,14 +40,6 @@ actions: ```ts import { GResponse } from "./sdk/envelopes/index"; -import { - MArray, - MArrayNullable, - MCollection, - MCollectionNullable, - MOne, - MOneNullable, -} from "./sdk/common/operators"; import { Money } from "../some/directory/Money"; import { buildUrl } from "./sdk/common/buildUrl"; import { diff --git a/examples/emi-web/src/content/docs/js/emi-enum.md b/examples/emi-web/src/content/docs/js/emi-enum.md index 222a79ba..19d1662a 100644 --- a/examples/emi-web/src/content/docs/js/emi-enum.md +++ b/examples/emi-web/src/content/docs/js/emi-enum.md @@ -4,12 +4,10 @@ sidebar: order: 4 --- - - Besides defining inline enums, emi definition allows for standalone enums which is really useful, for sharing enums between multiple files. - + ```yaml name: emiEnums enums: diff --git a/examples/emi-web/src/content/docs/js/emi-fields-auto-init.mdx b/examples/emi-web/src/content/docs/js/emi-fields-auto-init.mdx index 6f387b7a..0f132829 100644 --- a/examples/emi-web/src/content/docs/js/emi-fields-auto-init.mdx +++ b/examples/emi-web/src/content/docs/js/emi-fields-auto-init.mdx @@ -56,14 +56,7 @@ Emi compiler generates ts type and, class, with full getter, setters and validat ```ts -import { - MArray, - MArrayNullable, - MCollection, - MCollectionNullable, - MOne, - MOneNullable, -} from "./sdk/common/operators"; +import { MArray } from "./sdk/common/operators"; import { type PartialDeep } from "./sdk/common/fetchx"; import { withPrefix } from "./sdk/common/withPrefix"; /** @@ -138,9 +131,9 @@ export class AutoInitClassDto { * This field will be always an array * @type {AutoInitClassDto.Object1.Object2.Contacts} **/ - #contacts: InstanceType< - typeof AutoInitClassDto.Object1.Object2.Contacts - >[] = []; + #contacts: MArray< + InstanceType + > = MArray.of([]); /** * This field will be always an array * @returns {AutoInitClassDto.Object1.Object2.Contacts} @@ -153,24 +146,51 @@ export class AutoInitClassDto { * @type {AutoInitClassDto.Object1.Object2.Contacts} **/ set contacts( - value: InstanceType[], + value: + | MArray< + InstanceType + > + | InstanceType[], ) { - if (!Array.isArray(value) && !(value instanceof MCollection)) { + // When the passed value is already an array, we check if we need to + // cast the inner items into class instance. + if (Array.isArray(value)) { + if ( + value.length > 0 && + value[0] instanceof AutoInitClassDto.Object1.Object2.Contacts + ) { + this.#contacts = MArray.of(value); + } else { + this.#contacts = MArray.of( + value.map( + (item) => new AutoInitClassDto.Object1.Object2.Contacts(item), + ), + ); + } return; } - if ( - value.length > 0 && - value[0] instanceof AutoInitClassDto.Object1.Object2.Contacts - ) { + // If the instance is already an MArray, we assume it's all good. + if (value instanceof MArray) { this.#contacts = value; - } else { - this.#contacts = value.map( - (item) => new AutoInitClassDto.Object1.Object2.Contacts(item), - ); + return; } + // If the value is not array, and is not a MArray, we need to be consider, + // it might be eligible to be casted into MArray. + const { ok, value: mcastValue } = MArray.cast(value); + if (ok) { + this.#contacts = mcastValue as any; + return; + } + console.warn( + "Cannot assing value to contacts, because it needs MArray instance or an Array.", + ); } setContacts( - value: InstanceType[], + value: + | MArray< + InstanceType + > + | InstanceType[], ) { this.contacts = value; return this; diff --git a/examples/emi-web/src/content/docs/js/emi-float64-data-type.mdx b/examples/emi-web/src/content/docs/js/emi-float64-data-type.mdx index 88798fa7..4945630e 100644 --- a/examples/emi-web/src/content/docs/js/emi-float64-data-type.mdx +++ b/examples/emi-web/src/content/docs/js/emi-float64-data-type.mdx @@ -23,14 +23,6 @@ fields: - `float64?` allows `null` and `undefined`, defaulting to `undefined`. ```ts -import { - MArray, - MArrayNullable, - MCollection, - MCollectionNullable, - MOne, - MOneNullable, -} from "./sdk/common/operators"; import { type PartialDeep } from "./sdk/common/fetchx"; /** * The base class definition for myFloatClassDto @@ -207,14 +199,6 @@ fields: Defaults to `undefined`, but you can assign any float value or `null`. ```ts -import { - MArray, - MArrayNullable, - MCollection, - MCollectionNullable, - MOne, - MOneNullable, -} from "./sdk/common/operators"; import { type PartialDeep } from "./sdk/common/fetchx"; /** * The base class definition for myFloatClassDto diff --git a/examples/emi-web/src/content/docs/js/emi-int-data-type.mdx b/examples/emi-web/src/content/docs/js/emi-int-data-type.mdx index c71dd338..da3b74f4 100644 --- a/examples/emi-web/src/content/docs/js/emi-int-data-type.mdx +++ b/examples/emi-web/src/content/docs/js/emi-int-data-type.mdx @@ -23,14 +23,6 @@ fields: - `int?` allows `null` and `undefined`, with `undefined` as default. ```ts -import { - MArray, - MArrayNullable, - MCollection, - MCollectionNullable, - MOne, - MOneNullable, -} from "./sdk/common/operators"; import { type PartialDeep } from "./sdk/common/fetchx"; /** * The base class definition for myIntClassDto @@ -215,14 +207,6 @@ fields: Defaults to `undefined`, but you can assign `0`, any number, or `null`. ```ts -import { - MArray, - MArrayNullable, - MCollection, - MCollectionNullable, - MOne, - MOneNullable, -} from "./sdk/common/operators"; import { type PartialDeep } from "./sdk/common/fetchx"; /** * The base class definition for myIntClassDto diff --git a/examples/emi-web/src/content/docs/js/emi-javascript-web-socket.mdx b/examples/emi-web/src/content/docs/js/emi-javascript-web-socket.mdx index 6a57ef30..19597011 100644 --- a/examples/emi-web/src/content/docs/js/emi-javascript-web-socket.mdx +++ b/examples/emi-web/src/content/docs/js/emi-javascript-web-socket.mdx @@ -62,14 +62,6 @@ actions: ```ts -import { - MArray, - MArrayNullable, - MCollection, - MCollectionNullable, - MOne, - MOneNullable, -} from "./sdk/common/operators"; import { WebSocketX } from "./sdk/common/WebSocketX"; import { buildUrl } from "./sdk/common/buildUrl"; import { type PartialDeep } from "./sdk/common/fetchx"; diff --git a/examples/emi-web/src/content/docs/js/emi-nullable-object.md b/examples/emi-web/src/content/docs/js/emi-nullable-object.md index ad4842d4..0519ebfd 100644 --- a/examples/emi-web/src/content/docs/js/emi-nullable-object.md +++ b/examples/emi-web/src/content/docs/js/emi-nullable-object.md @@ -36,14 +36,7 @@ fields: ```ts -import { - MArray, - MArrayNullable, - MCollection, - MCollectionNullable, - MOne, - MOneNullable, -} from "./sdk/common/operators"; +import { MOne } from "./sdk/common/operators"; import { UncleDto } from "./UncleDto"; import { type PartialDeep } from "./sdk/common/fetchx"; import { withPrefix } from "./sdk/common/withPrefix"; @@ -129,7 +122,7 @@ export class NullableResponseActionDto { * Uncle is a separate dto, therefor we use that entity * @type {UncleDto} **/ - #firstUncle!: UncleDto; + #firstUncle!: MOne; /** * Uncle is a separate dto, therefor we use that entity * @returns {UncleDto} @@ -141,15 +134,17 @@ export class NullableResponseActionDto { * Uncle is a separate dto, therefor we use that entity * @type {UncleDto} **/ - set firstUncle(value: UncleDto) { + set firstUncle(value: MOne | InstanceType) { // For objects, the sub type needs to always be instance of the sub class. - if (value instanceof UncleDto) { + if (value instanceof MOne) { this.#firstUncle = value; + } else if (value instanceof UncleDto) { + this.#firstUncle = MOne.of(value); } else { - this.#firstUncle = new UncleDto(value); + this.#firstUncle = MOne.of(new UncleDto(value)); } } - setFirstUncle(value: UncleDto) { + setFirstUncle(value: MOne | InstanceType) { this.firstUncle = value; return this; } @@ -157,7 +152,7 @@ export class NullableResponseActionDto { * Second uncle is optional * @type {UncleDto} **/ - #secondUncle?: UncleDto | null = undefined; + #secondUncle?: MOne | null = undefined; /** * Second uncle is optional * @returns {UncleDto} @@ -169,15 +164,21 @@ export class NullableResponseActionDto { * Second uncle is optional * @type {UncleDto} **/ - set secondUncle(value: UncleDto | null | undefined) { + set secondUncle( + value: MOne | InstanceType | null | undefined, + ) { // For objects, the sub type needs to always be instance of the sub class. - if (value instanceof UncleDto) { + if (value instanceof MOne) { this.#secondUncle = value; + } else if (value instanceof UncleDto) { + this.#secondUncle = MOne.of(value); } else { - this.#secondUncle = new UncleDto(value); + this.#secondUncle = MOne.of(new UncleDto(value)); } } - setSecondUncle(value: UncleDto | null | undefined) { + setSecondUncle( + value: MOne | InstanceType | null | undefined, + ) { this.secondUncle = value; return this; } @@ -469,7 +470,7 @@ export class NullableResponseActionDto { this.mother = new NullableResponseActionDto.Mother(d.mother || {}); } if (!(d.firstUncle instanceof UncleDto)) { - this.firstUncle = new UncleDto(d.firstUncle || {}); + this.firstUncle = MOne.of(new UncleDto(d.firstUncle || {})); } } /** diff --git a/examples/emi-web/src/content/docs/js/emi-object-data-type.mdx b/examples/emi-web/src/content/docs/js/emi-object-data-type.mdx index 73852642..01756b3c 100644 --- a/examples/emi-web/src/content/docs/js/emi-object-data-type.mdx +++ b/examples/emi-web/src/content/docs/js/emi-object-data-type.mdx @@ -26,14 +26,6 @@ fields: - `object?` allows `null`/ `undefined`. ```ts -import { - MArray, - MArrayNullable, - MCollection, - MCollectionNullable, - MOne, - MOneNullable, -} from "./sdk/common/operators"; import { type PartialDeep } from "./sdk/common/fetchx"; import { withPrefix } from "./sdk/common/withPrefix"; /** @@ -358,14 +350,6 @@ fields: Defaults to `undefined`, but you can assign an object with required child fields or `null`. ```ts -import { - MArray, - MArrayNullable, - MCollection, - MCollectionNullable, - MOne, - MOneNullable, -} from "./sdk/common/operators"; import { type PartialDeep } from "./sdk/common/fetchx"; import { withPrefix } from "./sdk/common/withPrefix"; /** diff --git a/examples/emi-web/src/content/docs/js/emi-object.mdx b/examples/emi-web/src/content/docs/js/emi-object.mdx index 636eaa9d..9b8860b8 100644 --- a/examples/emi-web/src/content/docs/js/emi-object.mdx +++ b/examples/emi-web/src/content/docs/js/emi-object.mdx @@ -109,14 +109,6 @@ Emi compiler generates ts type and, class, with full getter, setters and validat ```ts -import { - MArray, - MArrayNullable, - MCollection, - MCollectionNullable, - MOne, - MOneNullable, -} from "./sdk/common/operators"; /** * The base class definition for anonymouse **/ diff --git a/examples/emi-web/src/content/docs/js/emi-static-fields.mdx b/examples/emi-web/src/content/docs/js/emi-static-fields.mdx index 4bfb9f69..0097fe2d 100644 --- a/examples/emi-web/src/content/docs/js/emi-static-fields.mdx +++ b/examples/emi-web/src/content/docs/js/emi-static-fields.mdx @@ -85,14 +85,7 @@ pass number as many as you want, and it would replace [:i] statements to have pr ```ts -import { - MArray, - MArrayNullable, - MCollection, - MCollectionNullable, - MOne, - MOneNullable, -} from "./sdk/common/operators"; +import { MArray } from "./sdk/common/operators"; import { withPrefix } from "./sdk/common/withPrefix"; /** * The base class definition for anonymouse @@ -125,7 +118,7 @@ export class Anonymouse { * * @type {Anonymouse.LoginHistory} **/ - #loginHistory = []; + #loginHistory = MArray.of([]); /** * * @returns {Anonymouse.LoginHistory} @@ -138,16 +131,33 @@ export class Anonymouse { * @type {Anonymouse.LoginHistory} **/ set loginHistory(value) { - if (!Array.isArray(value) && !(value instanceof MCollection)) { + // When the passed value is already an array, we check if we need to + // cast the inner items into class instance. + if (Array.isArray(value)) { + if (value.length > 0 && value[0] instanceof Anonymouse.LoginHistory) { + this.#loginHistory = MArray.of(value); + } else { + this.#loginHistory = MArray.of( + value.map((item) => new Anonymouse.LoginHistory(item)), + ); + } return; } - if (value.length > 0 && value[0] instanceof Anonymouse.LoginHistory) { + // If the instance is already an MArray, we assume it's all good. + if (value instanceof MArray) { this.#loginHistory = value; - } else { - this.#loginHistory = value.map( - (item) => new Anonymouse.LoginHistory(item), - ); + return; + } + // If the value is not array, and is not a MArray, we need to be consider, + // it might be eligible to be casted into MArray. + const { ok, value: mcastValue } = MArray.cast(value); + if (ok) { + this.#loginHistory = mcastValue; + return; } + console.warn( + "Cannot assing value to loginHistory, because it needs MArray instance or an Array.", + ); } setLoginHistory(value) { this.loginHistory = value; diff --git a/examples/emi-web/src/content/docs/js/emi-string-data-type.mdx b/examples/emi-web/src/content/docs/js/emi-string-data-type.mdx index fc7fdaf2..c57b2837 100644 --- a/examples/emi-web/src/content/docs/js/emi-string-data-type.mdx +++ b/examples/emi-web/src/content/docs/js/emi-string-data-type.mdx @@ -27,14 +27,6 @@ fields: ```ts -import { - MArray, - MArrayNullable, - MCollection, - MCollectionNullable, - MOne, - MOneNullable, -} from "./sdk/common/operators"; import { type PartialDeep } from "./sdk/common/fetchx"; /** * The base class definition for myStringClassDto @@ -215,14 +207,6 @@ fields: ```ts -import { - MArray, - MArrayNullable, - MCollection, - MCollectionNullable, - MOne, - MOneNullable, -} from "./sdk/common/operators"; import { type PartialDeep } from "./sdk/common/fetchx"; /** * The base class definition for myStringClassDto diff --git a/examples/js-sdk-kit/build/common/operators.js b/examples/js-sdk-kit/build/common/operators.js index 57b1d36f..a1feab5e 100644 --- a/examples/js-sdk-kit/build/common/operators.js +++ b/examples/js-sdk-kit/build/common/operators.js @@ -6,6 +6,20 @@ export class MOne { isNull() { return this.content === null; } + static cast(value) { + if (typeof value === "object" && (value === null || value === void 0 ? void 0 : value.__operation)) { + const res = MOne.of(value === null || value === void 0 ? void 0 : value.content); + res.operation = value === null || value === void 0 ? void 0 : value.__operation; + return { + ok: true, + value: res, + }; + } + return { + value: null, + ok: false, + }; + } isSelector() { return this.selector !== undefined && this.operation !== null; } @@ -35,10 +49,6 @@ export class MOne { return this.content; } } -// In javascript, nullability and undefined is already working perfectly fine. -// hence, maybe just giving back one is enough. -export class MOneNullable extends MOne { -} // Array describes how an incoming list should be applied to an existing one, // mirroring emigo.Array on the Go side. It lets a PATCH-style payload say // "replace the whole set" versus "append to the existing set". @@ -51,6 +61,20 @@ export class MArray { this.operation = "replace"; this.items = []; } + static cast(value) { + if (typeof value === "object" && (value === null || value === void 0 ? void 0 : value.__operation)) { + const res = MArray.of(value === null || value === void 0 ? void 0 : value.items); + res.operation = value === null || value === void 0 ? void 0 : value.__operation; + return { + ok: true, + value: res, + }; + } + return { + value: null, + ok: false, + }; + } isAppend() { return this.operation === "append"; } @@ -89,10 +113,6 @@ export class MArray { return this.items; } } -// In javascript, nullability and undefined is already working perfectly fine. -// hence, just extending Array is enough. -export class MArrayNullable extends Array { -} // Collection mirrors emigo.Collection on the Go side. Structurally it is the // same as Array — a list carrying a "replace"/"append" operation — but it is a // distinct field type: a collection holds a list of a target entity, whereas an @@ -105,6 +125,20 @@ export class MCollection { isAppend() { return this.operation === "append"; } + static cast(value) { + if (typeof value === "object" && (value === null || value === void 0 ? void 0 : value.__operation)) { + const res = MCollection.of(value === null || value === void 0 ? void 0 : value.items); + res.operation = value === null || value === void 0 ? void 0 : value.__operation; + return { + ok: true, + value: res, + }; + } + return { + value: null, + ok: false, + }; + } isReplace() { return this.operation === "replace"; } @@ -140,7 +174,3 @@ export class MCollection { return this.items; } } -// In javascript, nullability and undefined is already working perfectly fine. -// hence, just extending Collection is enough. -export class MCollectionNullable extends MCollection { -} diff --git a/examples/js-sdk-kit/src/common/operators.ts b/examples/js-sdk-kit/src/common/operators.ts index df3b8e99..6ad63403 100644 --- a/examples/js-sdk-kit/src/common/operators.ts +++ b/examples/js-sdk-kit/src/common/operators.ts @@ -8,6 +8,23 @@ export class MOne { return this.content === null; } + static cast(value: unknown): { ok: boolean; value: MOne | null } { + if (typeof value === "object" && (value as any)?.__operation) { + const res = MOne.of((value as any)?.content); + res.operation = (value as any)?.__operation; + + return { + ok: true, + value: res, + }; + } + + return { + value: null, + ok: false, + }; + } + isSelector(): boolean { return this.selector !== undefined && this.operation !== null; } @@ -45,10 +62,6 @@ export class MOne { } } -// In javascript, nullability and undefined is already working perfectly fine. -// hence, maybe just giving back one is enough. -export class MOneNullable extends MOne {} - // Array describes how an incoming list should be applied to an existing one, // mirroring emigo.Array on the Go side. It lets a PATCH-style payload say // "replace the whole set" versus "append to the existing set". @@ -60,6 +73,25 @@ export class MArray { private operation: "replace" | "append" = "replace"; private items: T[] = []; + static cast( + value: unknown, + ): { ok: boolean; value: MArray | null } { + if (typeof value === "object" && (value as any)?.__operation) { + const res = MArray.of((value as any)?.items); + res.operation = (value as any)?.__operation; + + return { + ok: true, + value: res, + }; + } + + return { + value: null, + ok: false, + }; + } + isAppend(): boolean { return this.operation === "append"; } @@ -108,10 +140,6 @@ export class MArray { } } -// In javascript, nullability and undefined is already working perfectly fine. -// hence, just extending Array is enough. -export class MArrayNullable extends Array {} - // Collection mirrors emigo.Collection on the Go side. Structurally it is the // same as Array — a list carrying a "replace"/"append" operation — but it is a // distinct field type: a collection holds a list of a target entity, whereas an @@ -124,6 +152,25 @@ export class MCollection { return this.operation === "append"; } + static cast( + value: unknown, + ): { ok: boolean; value: MCollection | null } { + if (typeof value === "object" && (value as any)?.__operation) { + const res = MCollection.of((value as any)?.items); + res.operation = (value as any)?.__operation; + + return { + ok: true, + value: res, + }; + } + + return { + value: null, + ok: false, + }; + } + isReplace(): boolean { return this.operation === "replace"; } @@ -167,7 +214,3 @@ export class MCollection { return this.items; } } - -// In javascript, nullability and undefined is already working perfectly fine. -// hence, just extending Collection is enough. -export class MCollectionNullable extends MCollection {} diff --git a/examples/js-test/reactclient/src/generated/AverageDto.ts b/examples/js-test/reactclient/src/generated/AverageDto.ts index 7335522c..f6c156bb 100644 --- a/examples/js-test/reactclient/src/generated/AverageDto.ts +++ b/examples/js-test/reactclient/src/generated/AverageDto.ts @@ -1,11 +1,3 @@ -import { - MArray, - MArrayNullable, - MCollection, - MCollectionNullable, - MOne, - MOneNullable, -} from "./sdk/common/operators"; import { type PartialDeep } from "./sdk/common/fetchx"; /** * The base class definition for averageDto diff --git a/examples/js-test/reactclient/src/generated/ComputeDto.ts b/examples/js-test/reactclient/src/generated/ComputeDto.ts index ab35a648..ca539af9 100644 --- a/examples/js-test/reactclient/src/generated/ComputeDto.ts +++ b/examples/js-test/reactclient/src/generated/ComputeDto.ts @@ -1,12 +1,4 @@ import { Decimal } from "decimal"; -import { - MArray, - MArrayNullable, - MCollection, - MCollectionNullable, - MOne, - MOneNullable, -} from "./sdk/common/operators"; import { type PartialDeep } from "./sdk/common/fetchx"; /** * The base class definition for computeDto diff --git a/examples/js-test/reactclient/src/generated/GetSinglePostAction.ts b/examples/js-test/reactclient/src/generated/GetSinglePostAction.ts index 434171d6..677007ed 100644 --- a/examples/js-test/reactclient/src/generated/GetSinglePostAction.ts +++ b/examples/js-test/reactclient/src/generated/GetSinglePostAction.ts @@ -1,11 +1,3 @@ -import { - MArray, - MArrayNullable, - MCollection, - MCollectionNullable, - MOne, - MOneNullable, -} from "./sdk/common/operators"; import { Money } from "../Money"; import { buildUrl } from "./sdk/common/buildUrl"; import { diff --git a/examples/js-test/reactclient/src/generated/SampleSseAction.ts b/examples/js-test/reactclient/src/generated/SampleSseAction.ts index e1212573..12e8d41d 100644 --- a/examples/js-test/reactclient/src/generated/SampleSseAction.ts +++ b/examples/js-test/reactclient/src/generated/SampleSseAction.ts @@ -1,11 +1,3 @@ -import { - MArray, - MArrayNullable, - MCollection, - MCollectionNullable, - MOne, - MOneNullable, -} from "./sdk/common/operators"; import { buildUrl } from "./sdk/common/buildUrl"; import { fetchx, diff --git a/examples/js-test/reactclient/src/generated/WebSocketOrgEchoAction.ts b/examples/js-test/reactclient/src/generated/WebSocketOrgEchoAction.ts index f4d17158..c00b01b5 100644 --- a/examples/js-test/reactclient/src/generated/WebSocketOrgEchoAction.ts +++ b/examples/js-test/reactclient/src/generated/WebSocketOrgEchoAction.ts @@ -1,11 +1,4 @@ -import { - MArray, - MArrayNullable, - MCollection, - MCollectionNullable, - MOne, - MOneNullable, -} from "./sdk/common/operators"; +import { MArray } from "./sdk/common/operators"; import { WebSocketX } from "./sdk/common/WebSocketX"; import { buildUrl } from "./sdk/common/buildUrl"; import { type PartialDeep } from "./sdk/common/fetchx"; @@ -205,9 +198,9 @@ export class WebSocketOrgEchoActionReq { * * @type {WebSocketOrgEchoActionReq.User.Item2array} **/ - #item2array: InstanceType< - typeof WebSocketOrgEchoActionReq.User.Item2array - >[] = []; + #item2array: MArray< + InstanceType + > = MArray.of([]); /** * * @returns {WebSocketOrgEchoActionReq.User.Item2array} @@ -220,24 +213,47 @@ export class WebSocketOrgEchoActionReq { * @type {WebSocketOrgEchoActionReq.User.Item2array} **/ set item2array( - value: InstanceType[], + value: + | MArray> + | InstanceType[], ) { - if (!Array.isArray(value) && !(value instanceof MCollection)) { + // When the passed value is already an array, we check if we need to + // cast the inner items into class instance. + if (Array.isArray(value)) { + if ( + value.length > 0 && + value[0] instanceof WebSocketOrgEchoActionReq.User.Item2array + ) { + this.#item2array = MArray.of(value); + } else { + this.#item2array = MArray.of( + value.map( + (item) => new WebSocketOrgEchoActionReq.User.Item2array(item), + ), + ); + } return; } - if ( - value.length > 0 && - value[0] instanceof WebSocketOrgEchoActionReq.User.Item2array - ) { + // If the instance is already an MArray, we assume it's all good. + if (value instanceof MArray) { this.#item2array = value; - } else { - this.#item2array = value.map( - (item) => new WebSocketOrgEchoActionReq.User.Item2array(item), - ); + return; } + // If the value is not array, and is not a MArray, we need to be consider, + // it might be eligible to be casted into MArray. + const { ok, value: mcastValue } = MArray.cast(value); + if (ok) { + this.#item2array = mcastValue as any; + return; + } + console.warn( + "Cannot assing value to item2array, because it needs MArray instance or an Array.", + ); } setItem2array( - value: InstanceType[], + value: + | MArray> + | InstanceType[], ) { this.item2array = value; return this; diff --git a/examples/js-test/reactclient/src/generated/sdk/common/operators.ts b/examples/js-test/reactclient/src/generated/sdk/common/operators.ts index df3b8e99..6ad63403 100644 --- a/examples/js-test/reactclient/src/generated/sdk/common/operators.ts +++ b/examples/js-test/reactclient/src/generated/sdk/common/operators.ts @@ -8,6 +8,23 @@ export class MOne { return this.content === null; } + static cast(value: unknown): { ok: boolean; value: MOne | null } { + if (typeof value === "object" && (value as any)?.__operation) { + const res = MOne.of((value as any)?.content); + res.operation = (value as any)?.__operation; + + return { + ok: true, + value: res, + }; + } + + return { + value: null, + ok: false, + }; + } + isSelector(): boolean { return this.selector !== undefined && this.operation !== null; } @@ -45,10 +62,6 @@ export class MOne { } } -// In javascript, nullability and undefined is already working perfectly fine. -// hence, maybe just giving back one is enough. -export class MOneNullable extends MOne {} - // Array describes how an incoming list should be applied to an existing one, // mirroring emigo.Array on the Go side. It lets a PATCH-style payload say // "replace the whole set" versus "append to the existing set". @@ -60,6 +73,25 @@ export class MArray { private operation: "replace" | "append" = "replace"; private items: T[] = []; + static cast( + value: unknown, + ): { ok: boolean; value: MArray | null } { + if (typeof value === "object" && (value as any)?.__operation) { + const res = MArray.of((value as any)?.items); + res.operation = (value as any)?.__operation; + + return { + ok: true, + value: res, + }; + } + + return { + value: null, + ok: false, + }; + } + isAppend(): boolean { return this.operation === "append"; } @@ -108,10 +140,6 @@ export class MArray { } } -// In javascript, nullability and undefined is already working perfectly fine. -// hence, just extending Array is enough. -export class MArrayNullable extends Array {} - // Collection mirrors emigo.Collection on the Go side. Structurally it is the // same as Array — a list carrying a "replace"/"append" operation — but it is a // distinct field type: a collection holds a list of a target entity, whereas an @@ -124,6 +152,25 @@ export class MCollection { return this.operation === "append"; } + static cast( + value: unknown, + ): { ok: boolean; value: MCollection | null } { + if (typeof value === "object" && (value as any)?.__operation) { + const res = MCollection.of((value as any)?.items); + res.operation = (value as any)?.__operation; + + return { + ok: true, + value: res, + }; + } + + return { + value: null, + ok: false, + }; + } + isReplace(): boolean { return this.operation === "replace"; } @@ -167,7 +214,3 @@ export class MCollection { return this.items; } } - -// In javascript, nullability and undefined is already working perfectly fine. -// hence, just extending Collection is enough. -export class MCollectionNullable extends MCollection {} diff --git a/examples/js/cases/User.ts b/examples/js/cases/User.ts new file mode 100644 index 00000000..d89a4adc --- /dev/null +++ b/examples/js/cases/User.ts @@ -0,0 +1,92 @@ +import { type PartialDeep } from './sdk/common/fetchx'; +/** + * The base class definition for user + **/ +export class User { + constructor(data: unknown = undefined) { + if (data === null || data === undefined) { + return; + } + if (typeof data === "string") { + this.applyFromObject(JSON.parse(data)); + } else if (this.#isJsonAppliable(data)) { + this.applyFromObject(data); + } else { + throw new Error("Instance cannot be created on an unknown value, check the content being passed. got: " + typeof data); + } + } + #isJsonAppliable(obj: unknown) { + const g = globalThis as unknown as { Buffer: any; Blob: any }; + const isBuffer = + typeof g.Buffer !== "undefined" && + typeof g.Buffer.isBuffer === "function" && + g.Buffer.isBuffer(obj); + const isBlob = + typeof g.Blob !== "undefined" && obj instanceof g.Blob; + return ( + obj && + typeof obj === "object" && + !Array.isArray(obj) && + !isBuffer && + !(obj instanceof ArrayBuffer) && + !isBlob + ); + } + /** + * casts the fields of a javascript object into the class properties one by one + **/ + applyFromObject(data = {}) { + const d = data as Partial; + } + /** + * Special toJSON override, since the field are private, + * Json stringify won't see them unless we mention it explicitly. + **/ + toJSON() { + return { + }; + } + toString() { + return JSON.stringify(this); + } + static get Fields() { + return { + } + } + /** + * Creates an instance of User, and possibleDtoObject + * needs to satisfy the type requirement fully, otherwise typescript compile would + * be complaining. + **/ + static from(possibleDtoObject: UserType) { + return new User(possibleDtoObject); + } + /** + * Creates an instance of User, and partialDtoObject + * needs to satisfy the type, but partially, and rest of the content would + * be constructed according to data types and nullability. + **/ + static with(partialDtoObject: PartialDeep) { + return new User(partialDtoObject); + } + copyWith(partial: PartialDeep): InstanceType { + return new User ({ ...this.toJSON(), ...partial }); + } + clone(): InstanceType { + return new User(this.toJSON()); + } +} +export abstract class UserFactory { + abstract create(data: unknown): User; +} + /** + * The base type definition for user + **/ + export type UserType = { + } +// eslint-disable-next-line @typescript-eslint/no-namespace +export namespace UserType { +} + + function isPlausibleObject(v: any) { return false } + \ No newline at end of file diff --git a/examples/js/cases/emi-data-type.output.ts b/examples/js/cases/emi-data-type.output.ts index 311a5245..2ba540d4 100644 --- a/examples/js/cases/emi-data-type.output.ts +++ b/examples/js/cases/emi-data-type.output.ts @@ -1,4 +1,3 @@ -import { MArray, MArrayNullable, MCollection, MCollectionNullable, MOne, MOneNullable } from './sdk/common/operators'; import { type PartialDeep } from './sdk/common/fetchx'; /** * The base class definition for anonymouse diff --git a/examples/js/cases/emi-js-array-one-collection-wrapper.output.js b/examples/js/cases/emi-js-array-one-collection-wrapper.output.js new file mode 100644 index 00000000..fd017c36 --- /dev/null +++ b/examples/js/cases/emi-js-array-one-collection-wrapper.output.js @@ -0,0 +1,431 @@ +import { MArray, MCollection, MOne } from './sdk/common/operators'; +import { User } from './User'; +import { withPrefix } from './sdk/common/withPrefix'; +/** + * The base class definition for anonymouse + **/ +export class Anonymouse { + /** + * array field, non-nullable + * @type {Anonymouse.ArrayField} + **/ + #arrayField = MArray.of([]) + /** + * array field, non-nullable + * @returns {Anonymouse.ArrayField} + **/ +get arrayField () { return this.#arrayField } +/** + * array field, non-nullable + * @type {Anonymouse.ArrayField} + **/ +set arrayField (value) { + // When the passed value is already an array, we check if we need to + // cast the inner items into class instance. + if (Array.isArray(value)) { + if (value.length > 0 && value[0] instanceof Anonymouse.ArrayField) { + this.#arrayField = MArray.of(value); + } else { + this.#arrayField = MArray.of( + value.map((item) => new Anonymouse.ArrayField(item)), + ); + } + return; + } + // If the instance is already an MArray, we assume it's all good. + if (value instanceof MArray) { + this.#arrayField = value; + return; + } + // If the value is not array, and is not a MArray, we need to be consider, + // it might be eligible to be casted into MArray. + const { ok, value: mcastValue } = MArray.cast(value); + if (ok) { + this.#arrayField = mcastValue; + return; + } + console.warn( + "Cannot assing value to arrayField, because it needs MArray instance or an Array.", + ); +} +setArrayField (value) { + this.arrayField = value + return this +} + /** + * arrayNullable field, non-nullable + * @type {any} + **/ + #arrayNullableField + /** + * arrayNullable field, non-nullable + * @returns {any} + **/ +get arrayNullableField () { return this.#arrayNullableField } +/** + * arrayNullable field, non-nullable + * @type {any} + **/ +set arrayNullableField (value) { + this.#arrayNullableField = value; +} +setArrayNullableField (value) { + this.arrayNullableField = value + return this +} + /** + * collection field, non-nullable + * @type {User[]} + **/ + #collectionField = MCollection.of([]) + /** + * collection field, non-nullable + * @returns {User[]} + **/ +get collectionField () { return this.#collectionField } +/** + * collection field, non-nullable + * @type {User[]} + **/ +set collectionField (value) { + // When the passed value is already an array, we check if we need to + // cast the inner items into class instance. + if (Array.isArray(value)) { + if (value.length > 0 && value[0] instanceof User) { + this.#collectionField = MCollection.of(value); + } else { + this.#collectionField = MCollection.of( + value.map((item) => new User(item)), + ); + } + return; + } + // If the instance is already an MCollection, we assume it's all good. + if (value instanceof MCollection) { + this.#collectionField = value; + return; + } + // If the value is not array, and is not a MCollection, we need to be consider, + // it might be eligible to be casted into MCollection. + const { ok, value: mcastValue } = MCollection.cast(value); + if (ok) { + this.#collectionField = mcastValue; + return; + } + console.warn( + "Cannot assing value to collectionField, because it needs MCollection instance or an Array.", + ); +} +setCollectionField (value) { + this.collectionField = value + return this +} + /** + * collectionNullable field, non-nullable + * @type {User[]} + **/ + #collectionNullableField = undefined + /** + * collectionNullable field, non-nullable + * @returns {User[]} + **/ +get collectionNullableField () { return this.#collectionNullableField } +/** + * collectionNullable field, non-nullable + * @type {User[]} + **/ +set collectionNullableField (value) { + // For nullable collection, we allow explicit undefined or null values + if (value === null || value === undefined) { + this.#collectionNullableField = value; + return + } + // When the passed value is already an array, we check if we need to + // cast the inner items into class instance. + if (Array.isArray(value)) { + if (value.length > 0 && value[0] instanceof User) { + this.#collectionNullableField = MCollection.of(value); + } else { + this.#collectionNullableField = MCollection.of( + value.map((item) => new User(item)), + ); + } + return; + } + // If the instance is already an MCollection, we assume it's all good. + if (value instanceof MCollection) { + this.#collectionNullableField = value; + return; + } + // If the value is not array, and is not a MCollection, we need to be consider, + // it might be eligible to be casted into MCollection. + const { ok, value: mcastValue } = MCollection.cast(value); + if (ok) { + this.#collectionNullableField = mcastValue; + return; + } + console.warn( + "Cannot assing value to collectionNullableField, because it needs MCollection instance or an Array.", + ); +} +setCollectionNullableField (value) { + this.collectionNullableField = value + return this +} + /** + * one field, non-nullable + * @type {User} + **/ + #oneField + /** + * one field, non-nullable + * @returns {User} + **/ +get oneField () { return this.#oneField } +/** + * one field, non-nullable + * @type {User} + **/ +set oneField (value) { + // For objects, the sub type needs to always be instance of the sub class. + if (value instanceof MOne) { + this.#oneField = value + } else if (value instanceof User) { + this.#oneField = MOne.of(value) + } else { + this.#oneField = MOne.of(new User(value)) + } +} +setOneField (value) { + this.oneField = value + return this +} + /** + * oneNullable field, non-nullable + * @type {User} + **/ + #oneNullableField = undefined + /** + * oneNullable field, non-nullable + * @returns {User} + **/ +get oneNullableField () { return this.#oneNullableField } +/** + * oneNullable field, non-nullable + * @type {User} + **/ +set oneNullableField (value) { + // For objects, the sub type needs to always be instance of the sub class. + if (value instanceof MOne) { + this.#oneNullableField = value + } else if (value instanceof User) { + this.#oneNullableField = MOne.of(value) + } else { + this.#oneNullableField = MOne.of(new User(value)) + } +} +setOneNullableField (value) { + this.oneNullableField = value + return this +} +/** + * The base class definition for arrayField + **/ +static ArrayField = class ArrayField { + constructor(data) { + if (data === null || data === undefined) { + return; + } + if (typeof data === "string") { + this.applyFromObject(JSON.parse(data)); + } else if (this.#isJsonAppliable(data)) { + this.applyFromObject(data); + } else { + throw new Error("Instance cannot be created on an unknown value, check the content being passed. got: " + typeof data); + } + } + #isJsonAppliable(obj) { + const g = globalThis + const isBuffer = + typeof g.Buffer !== "undefined" && + typeof g.Buffer.isBuffer === "function" && + g.Buffer.isBuffer(obj); + const isBlob = + typeof g.Blob !== "undefined" && obj instanceof g.Blob; + return ( + obj && + typeof obj === "object" && + !Array.isArray(obj) && + !isBuffer && + !(obj instanceof ArrayBuffer) && + !isBlob + ); + } + /** + * casts the fields of a javascript object into the class properties one by one + **/ + applyFromObject(data = {}) { + const d = data; + } + /** + * Special toJSON override, since the field are private, + * Json stringify won't see them unless we mention it explicitly. + **/ + toJSON() { + return { + }; + } + toString() { + return JSON.stringify(this); + } + static get Fields() { + return { + } + } + /** + * Creates an instance of Anonymouse.ArrayField, and possibleDtoObject + * needs to satisfy the type requirement fully, otherwise typescript compile would + * be complaining. + **/ + static from(possibleDtoObject) { + return new Anonymouse.ArrayField(possibleDtoObject); + } + /** + * Creates an instance of Anonymouse.ArrayField, and partialDtoObject + * needs to satisfy the type, but partially, and rest of the content would + * be constructed according to data types and nullability. + **/ + static with(partialDtoObject) { + return new Anonymouse.ArrayField(partialDtoObject); + } + copyWith(partial) { + return new Anonymouse.ArrayField ({ ...this.toJSON(), ...partial }); + } + clone() { + return new Anonymouse.ArrayField(this.toJSON()); + } +} + constructor(data) { + if (data === null || data === undefined) { + this.#lateInitFields(); + return; + } + if (typeof data === "string") { + this.applyFromObject(JSON.parse(data)); + } else if (this.#isJsonAppliable(data)) { + this.applyFromObject(data); + } else { + throw new Error("Instance cannot be created on an unknown value, check the content being passed. got: " + typeof data); + } + } + #isJsonAppliable(obj) { + const g = globalThis + const isBuffer = + typeof g.Buffer !== "undefined" && + typeof g.Buffer.isBuffer === "function" && + g.Buffer.isBuffer(obj); + const isBlob = + typeof g.Blob !== "undefined" && obj instanceof g.Blob; + return ( + obj && + typeof obj === "object" && + !Array.isArray(obj) && + !isBuffer && + !(obj instanceof ArrayBuffer) && + !isBlob + ); + } + /** + * casts the fields of a javascript object into the class properties one by one + **/ + applyFromObject(data = {}) { + const d = data; + if (d.arrayField !== undefined) { this.arrayField = d.arrayField } + if (d.arrayNullableField !== undefined) { this.arrayNullableField = d.arrayNullableField } + if (d.collectionField !== undefined) { this.collectionField = d.collectionField } + if (d.collectionNullableField !== undefined) { this.collectionNullableField = d.collectionNullableField } + if (d.oneField !== undefined) { this.oneField = d.oneField } + if (d.oneNullableField !== undefined) { this.oneNullableField = d.oneNullableField } + this.#lateInitFields(data) + } + /** + * These are the class instances, which need to be initialised, regardless of the constructor incoming data + **/ + #lateInitFields(data = {}) { + const d = data; + if (!(d.oneField instanceof User)) { this.oneField = MOne.of(new User(d.oneField || {})) } + } + /** + * Special toJSON override, since the field are private, + * Json stringify won't see them unless we mention it explicitly. + **/ + toJSON() { + return { + arrayField: this.#arrayField, + arrayNullableField: this.#arrayNullableField, + collectionField: this.#collectionField, + collectionNullableField: this.#collectionNullableField, + oneField: this.#oneField, + oneNullableField: this.#oneNullableField, + }; + } + toString() { + return JSON.stringify(this); + } + static get Fields() { + return { + arrayField$: 'arrayField', +get arrayField() { + return withPrefix( + "arrayField[:i]", + Anonymouse.ArrayField.Fields + ); + }, + arrayNullableField: 'arrayNullableField', + collectionField$: 'collectionField', +get collectionField() { + return withPrefix( + "collectionField[:i]", + User.Fields + ); + }, + collectionNullableField$: 'collectionNullableField', +get collectionNullableField() { + return withPrefix( + "collectionNullableField", + User.Fields + ); + }, + oneField$: 'oneField', +get oneField() { + return withPrefix( + "oneField", + User.Fields + ); + }, + oneNullableField: 'oneNullableField', + } + } + /** + * Creates an instance of Anonymouse, and possibleDtoObject + * needs to satisfy the type requirement fully, otherwise typescript compile would + * be complaining. + **/ + static from(possibleDtoObject) { + return new Anonymouse(possibleDtoObject); + } + /** + * Creates an instance of Anonymouse, and partialDtoObject + * needs to satisfy the type, but partially, and rest of the content would + * be constructed according to data types and nullability. + **/ + static with(partialDtoObject) { + return new Anonymouse(partialDtoObject); + } + copyWith(partial) { + return new Anonymouse ({ ...this.toJSON(), ...partial }); + } + clone() { + return new Anonymouse(this.toJSON()); + } +} \ No newline at end of file diff --git a/examples/js/cases/emi-js-array-one-collection-wrapper.output.ts b/examples/js/cases/emi-js-array-one-collection-wrapper.output.ts new file mode 100644 index 00000000..831b450c --- /dev/null +++ b/examples/js/cases/emi-js-array-one-collection-wrapper.output.ts @@ -0,0 +1,517 @@ +import { MArray, MCollection, MOne } from "./sdk/common/operators"; +import { User } from "./User"; +import { type PartialDeep } from "./sdk/common/fetchx"; +import { withPrefix } from "./sdk/common/withPrefix"; +/** + * The base class definition for anonymouse + **/ +export class Anonymouse { + /** + * array field, non-nullable + * @type {Anonymouse.ArrayField} + **/ + #arrayField: MArray> = MArray.of( + [], + ); + /** + * array field, non-nullable + * @returns {Anonymouse.ArrayField} + **/ + get arrayField() { + return this.#arrayField; + } + /** + * array field, non-nullable + * @type {Anonymouse.ArrayField} + **/ + set arrayField( + value: + | MArray> + | InstanceType[], + ) { + // When the passed value is already an array, we check if we need to + // cast the inner items into class instance. + if (Array.isArray(value)) { + if (value.length > 0 && value[0] instanceof Anonymouse.ArrayField) { + this.#arrayField = MArray.of(value); + } else { + this.#arrayField = MArray.of( + value.map((item) => new Anonymouse.ArrayField(item)), + ); + } + return; + } + // If the instance is already an MArray, we assume it's all good. + if (value instanceof MArray) { + this.#arrayField = value; + return; + } + // If the value is not array, and is not a MArray, we need to be consider, + // it might be eligible to be casted into MArray. + const { ok, value: mcastValue } = MArray.cast(value); + if (ok) { + this.#arrayField = mcastValue as any; + return; + } + console.warn( + "Cannot assing value to arrayField, because it needs MArray instance or an Array.", + ); + } + setArrayField( + value: + | MArray> + | InstanceType[], + ) { + this.arrayField = value; + return this; + } + /** + * arrayNullable field, non-nullable + * @type {any} + **/ + #arrayNullableField!: any; + /** + * arrayNullable field, non-nullable + * @returns {any} + **/ + get arrayNullableField() { + return this.#arrayNullableField; + } + /** + * arrayNullable field, non-nullable + * @type {any} + **/ + set arrayNullableField(value: any) { + this.#arrayNullableField = value; + } + setArrayNullableField(value: any) { + this.arrayNullableField = value; + return this; + } + /** + * collection field, non-nullable + * @type {User[]} + **/ + #collectionField: MCollection = MCollection.of([]); + /** + * collection field, non-nullable + * @returns {User[]} + **/ + get collectionField() { + return this.#collectionField; + } + /** + * collection field, non-nullable + * @type {User[]} + **/ + set collectionField(value: MCollection | InstanceType[]) { + // When the passed value is already an array, we check if we need to + // cast the inner items into class instance. + if (Array.isArray(value)) { + if (value.length > 0 && value[0] instanceof User) { + this.#collectionField = MCollection.of(value); + } else { + this.#collectionField = MCollection.of( + value.map((item) => new User(item)), + ); + } + return; + } + // If the instance is already an MCollection, we assume it's all good. + if (value instanceof MCollection) { + this.#collectionField = value; + return; + } + // If the value is not array, and is not a MCollection, we need to be consider, + // it might be eligible to be casted into MCollection. + const { ok, value: mcastValue } = MCollection.cast(value); + if (ok) { + this.#collectionField = mcastValue as any; + return; + } + console.warn( + "Cannot assing value to collectionField, because it needs MCollection instance or an Array.", + ); + } + setCollectionField(value: MCollection | InstanceType[]) { + this.collectionField = value; + return this; + } + /** + * collectionNullable field, non-nullable + * @type {User[]} + **/ + #collectionNullableField?: MCollection | null = undefined; + /** + * collectionNullable field, non-nullable + * @returns {User[]} + **/ + get collectionNullableField() { + return this.#collectionNullableField; + } + /** + * collectionNullable field, non-nullable + * @type {User[]} + **/ + set collectionNullableField( + value: MCollection | InstanceType[] | null | undefined, + ) { + // For nullable collection, we allow explicit undefined or null values + if (value === null || value === undefined) { + this.#collectionNullableField = value; + return; + } + // When the passed value is already an array, we check if we need to + // cast the inner items into class instance. + if (Array.isArray(value)) { + if (value.length > 0 && value[0] instanceof User) { + this.#collectionNullableField = MCollection.of(value); + } else { + this.#collectionNullableField = MCollection.of( + value.map((item) => new User(item)), + ); + } + return; + } + // If the instance is already an MCollection, we assume it's all good. + if (value instanceof MCollection) { + this.#collectionNullableField = value; + return; + } + // If the value is not array, and is not a MCollection, we need to be consider, + // it might be eligible to be casted into MCollection. + const { ok, value: mcastValue } = MCollection.cast(value); + if (ok) { + this.#collectionNullableField = mcastValue as any; + return; + } + console.warn( + "Cannot assing value to collectionNullableField, because it needs MCollection instance or an Array.", + ); + } + setCollectionNullableField( + value: MCollection | InstanceType[] | null | undefined, + ) { + this.collectionNullableField = value; + return this; + } + /** + * one field, non-nullable + * @type {User} + **/ + #oneField!: MOne; + /** + * one field, non-nullable + * @returns {User} + **/ + get oneField() { + return this.#oneField; + } + /** + * one field, non-nullable + * @type {User} + **/ + set oneField(value: MOne | InstanceType) { + // For objects, the sub type needs to always be instance of the sub class. + if (value instanceof MOne) { + this.#oneField = value; + } else if (value instanceof User) { + this.#oneField = MOne.of(value); + } else { + this.#oneField = MOne.of(new User(value)); + } + } + setOneField(value: MOne | InstanceType) { + this.oneField = value; + return this; + } + /** + * oneNullable field, non-nullable + * @type {User} + **/ + #oneNullableField?: MOne | null = undefined; + /** + * oneNullable field, non-nullable + * @returns {User} + **/ + get oneNullableField() { + return this.#oneNullableField; + } + /** + * oneNullable field, non-nullable + * @type {User} + **/ + set oneNullableField( + value: MOne | InstanceType | null | undefined, + ) { + // For objects, the sub type needs to always be instance of the sub class. + if (value instanceof MOne) { + this.#oneNullableField = value; + } else if (value instanceof User) { + this.#oneNullableField = MOne.of(value); + } else { + this.#oneNullableField = MOne.of(new User(value)); + } + } + setOneNullableField( + value: MOne | InstanceType | null | undefined, + ) { + this.oneNullableField = value; + return this; + } + /** + * The base class definition for arrayField + **/ + static ArrayField = class ArrayField { + constructor(data: unknown = undefined) { + if (data === null || data === undefined) { + return; + } + if (typeof data === "string") { + this.applyFromObject(JSON.parse(data)); + } else if (this.#isJsonAppliable(data)) { + this.applyFromObject(data); + } else { + throw new Error( + "Instance cannot be created on an unknown value, check the content being passed. got: " + + typeof data, + ); + } + } + #isJsonAppliable(obj: unknown) { + const g = globalThis as unknown as { Buffer: any; Blob: any }; + const isBuffer = + typeof g.Buffer !== "undefined" && + typeof g.Buffer.isBuffer === "function" && + g.Buffer.isBuffer(obj); + const isBlob = typeof g.Blob !== "undefined" && obj instanceof g.Blob; + return ( + obj && + typeof obj === "object" && + !Array.isArray(obj) && + !isBuffer && + !(obj instanceof ArrayBuffer) && + !isBlob + ); + } + /** + * casts the fields of a javascript object into the class properties one by one + **/ + applyFromObject(data = {}) { + const d = data as Partial; + } + /** + * Special toJSON override, since the field are private, + * Json stringify won't see them unless we mention it explicitly. + **/ + toJSON() { + return {}; + } + toString() { + return JSON.stringify(this); + } + static get Fields() { + return {}; + } + /** + * Creates an instance of Anonymouse.ArrayField, and possibleDtoObject + * needs to satisfy the type requirement fully, otherwise typescript compile would + * be complaining. + **/ + static from(possibleDtoObject: AnonymouseType.ArrayFieldType) { + return new Anonymouse.ArrayField(possibleDtoObject); + } + /** + * Creates an instance of Anonymouse.ArrayField, and partialDtoObject + * needs to satisfy the type, but partially, and rest of the content would + * be constructed according to data types and nullability. + **/ + static with(partialDtoObject: PartialDeep) { + return new Anonymouse.ArrayField(partialDtoObject); + } + copyWith( + partial: PartialDeep, + ): InstanceType { + return new Anonymouse.ArrayField({ ...this.toJSON(), ...partial }); + } + clone(): InstanceType { + return new Anonymouse.ArrayField(this.toJSON()); + } + }; + constructor(data: unknown = undefined) { + if (data === null || data === undefined) { + this.#lateInitFields(); + return; + } + if (typeof data === "string") { + this.applyFromObject(JSON.parse(data)); + } else if (this.#isJsonAppliable(data)) { + this.applyFromObject(data); + } else { + throw new Error( + "Instance cannot be created on an unknown value, check the content being passed. got: " + + typeof data, + ); + } + } + #isJsonAppliable(obj: unknown) { + const g = globalThis as unknown as { Buffer: any; Blob: any }; + const isBuffer = + typeof g.Buffer !== "undefined" && + typeof g.Buffer.isBuffer === "function" && + g.Buffer.isBuffer(obj); + const isBlob = typeof g.Blob !== "undefined" && obj instanceof g.Blob; + return ( + obj && + typeof obj === "object" && + !Array.isArray(obj) && + !isBuffer && + !(obj instanceof ArrayBuffer) && + !isBlob + ); + } + /** + * casts the fields of a javascript object into the class properties one by one + **/ + applyFromObject(data = {}) { + const d = data as Partial; + if (d.arrayField !== undefined) { + this.arrayField = d.arrayField; + } + if (d.arrayNullableField !== undefined) { + this.arrayNullableField = d.arrayNullableField; + } + if (d.collectionField !== undefined) { + this.collectionField = d.collectionField; + } + if (d.collectionNullableField !== undefined) { + this.collectionNullableField = d.collectionNullableField; + } + if (d.oneField !== undefined) { + this.oneField = d.oneField; + } + if (d.oneNullableField !== undefined) { + this.oneNullableField = d.oneNullableField; + } + this.#lateInitFields(data); + } + /** + * These are the class instances, which need to be initialised, regardless of the constructor incoming data + **/ + #lateInitFields(data = {}) { + const d = data as Partial; + if (!(d.oneField instanceof User)) { + this.oneField = MOne.of(new User(d.oneField || {})); + } + } + /** + * Special toJSON override, since the field are private, + * Json stringify won't see them unless we mention it explicitly. + **/ + toJSON() { + return { + arrayField: this.#arrayField, + arrayNullableField: this.#arrayNullableField, + collectionField: this.#collectionField, + collectionNullableField: this.#collectionNullableField, + oneField: this.#oneField, + oneNullableField: this.#oneNullableField, + }; + } + toString() { + return JSON.stringify(this); + } + static get Fields() { + return { + arrayField$: "arrayField", + get arrayField() { + return withPrefix("arrayField[:i]", Anonymouse.ArrayField.Fields); + }, + arrayNullableField: "arrayNullableField", + collectionField$: "collectionField", + get collectionField() { + return withPrefix("collectionField[:i]", User.Fields); + }, + collectionNullableField$: "collectionNullableField", + get collectionNullableField() { + return withPrefix("collectionNullableField", User.Fields); + }, + oneField$: "oneField", + get oneField() { + return withPrefix("oneField", User.Fields); + }, + oneNullableField: "oneNullableField", + }; + } + /** + * Creates an instance of Anonymouse, and possibleDtoObject + * needs to satisfy the type requirement fully, otherwise typescript compile would + * be complaining. + **/ + static from(possibleDtoObject: AnonymouseType) { + return new Anonymouse(possibleDtoObject); + } + /** + * Creates an instance of Anonymouse, and partialDtoObject + * needs to satisfy the type, but partially, and rest of the content would + * be constructed according to data types and nullability. + **/ + static with(partialDtoObject: PartialDeep) { + return new Anonymouse(partialDtoObject); + } + copyWith( + partial: PartialDeep, + ): InstanceType { + return new Anonymouse({ ...this.toJSON(), ...partial }); + } + clone(): InstanceType { + return new Anonymouse(this.toJSON()); + } +} +export abstract class AnonymouseFactory { + abstract create(data: unknown): Anonymouse; +} +/** + * The base type definition for anonymouse + **/ +export type AnonymouseType = { + /** + * array field, non-nullable + * @type {any[]} + **/ + arrayField: any[]; + /** + * arrayNullable field, non-nullable + * @type {any} + **/ + arrayNullableField: any; + /** + * collection field, non-nullable + * @type {User[]} + **/ + collectionField: User[]; + /** + * collectionNullable field, non-nullable + * @type {User[]} + **/ + collectionNullableField?: User[]; + /** + * one field, non-nullable + * @type {User} + **/ + oneField: User; + /** + * oneNullable field, non-nullable + * @type {User} + **/ + oneNullableField?: User; +}; +// eslint-disable-next-line @typescript-eslint/no-namespace +export namespace AnonymouseType { + /** + * The base type definition for arrayFieldType + **/ + export type ArrayFieldType = {}; + // eslint-disable-next-line @typescript-eslint/no-namespace + export namespace ArrayFieldType {} +} diff --git a/examples/js/cases/emi-js-array-one-collection-wrapper.test.ts b/examples/js/cases/emi-js-array-one-collection-wrapper.test.ts new file mode 100644 index 00000000..94d37cd1 --- /dev/null +++ b/examples/js/cases/emi-js-array-one-collection-wrapper.test.ts @@ -0,0 +1,329 @@ +import { writeFileSync } from "fs"; +import path from "path"; +import { describe, expect, it } from "vitest"; +import { runEmiActionTs, runEmiActionTsNoCheck } from "../common"; + +describe("Data types with wrappers, one, collection, array need to work as intended.", () => { + const { resp: userResp } = runEmiActionTs("jsGenObject", [], { + Flags: JSON.stringify({ name: "User" }), + Tags: "react,typescript", + }); + + writeFileSync(path.join(__dirname + "/User.ts"), userResp); + + const fieldsMap = { + arrayField: { + type: "array", + name: "arrayField", + description: "array field, non-nullable", + $jstype: "array", + $initializerKind: "ArrayLiteral", + }, + arrayNullableField: { + type: "arrayNullable", + name: "arrayNullableField", + description: "arrayNullable field, non-nullable", + $jstype: "array?", + $initializerKind: "ArrayNullableLiteral", + }, + collectionField: { + type: "collection", + name: "collectionField", + target: "User", + description: "collection field, non-nullable", + $jstype: "collection", + $initializerKind: "collectionLiteral", + }, + collectionNullableField: { + type: "collection?", + name: "collectionNullableField", + description: "collectionNullable field, non-nullable", + target: "User", + $jstype: "collection?", + $initializerKind: "collectionNullableLiteral", + }, + oneField: { + type: "one", + name: "oneField", + target: "User", + description: "one field, non-nullable", + $jstype: "one", + $initializerKind: "oneLiteral", + }, + oneNullableField: { + type: "one?", + name: "oneNullableField", + description: "oneNullable field, non-nullable", + target: "User", + $jstype: "one?", + $initializerKind: "oneNullableLiteral", + }, + }; + const fields = Object.keys(fieldsMap).map((key) => ({ + name: key, + ...fieldsMap[key], + })); + + /// Now how to import in vite test the created typescript file, I want import Anonymouse class. + // But I do not want to do it in head of document, because file is generated after we wrtite it not before + + it("should be able to create an instance of it on typescript generation", async () => { + const { resp } = runEmiActionTsNoCheck("jsGenObject", fields, { + Flags: JSON.stringify({ name: "Anonymouse" }), + Tags: "react,typescript", + }); + + const output = path.join(__filename.replace(".test.ts", ".output.ts")); + writeFileSync(output, resp); + + const mod = await import(output); + + const { Anonymouse } = mod; + + expect(Anonymouse).toBeDefined(); + + const instance = new Anonymouse({ + arrayField: { + __operation: "append", + items: [new Anonymouse.ArrayField(), new Anonymouse.ArrayField()], + }, + }); + + expect(instance).toBeInstanceOf(Anonymouse); + }); + + it("should be able to create an instance of it on javascript generation", async () => { + const { resp } = runEmiActionTsNoCheck("jsGenObject", fields, { + Flags: JSON.stringify({ name: "Anonymouse" }), + Tags: "react", + }); + + const output = path.join(__filename.replace(".test.ts", ".output.js")); + writeFileSync(output, resp); + + const mod = await import(output); + + const { Anonymouse } = mod; + + expect(Anonymouse).toBeDefined(); + + const instance = new Anonymouse({ + arrayField: { + __operation: "append", + items: [new Anonymouse.ArrayField(), new Anonymouse.ArrayField()], + }, + }); + + expect(instance).toBeInstanceOf(Anonymouse); + }); + + // Generate the runtime (plain JS) module once, at collection time, so every + // scenario below can import the very same class. We deliberately do NOT reuse + // the `.output.js` written inside the test above — that one is produced at run + // time, so depending on it here would couple us to test execution order. + const scenarioOutput = path.join(__dirname, "scenarios.output.js"); + writeFileSync( + scenarioOutput, + runEmiActionTsNoCheck("jsGenObject", fields, { + Flags: JSON.stringify({ name: "Anonymouse" }), + Tags: "react", + }).resp, + ); + + // Lazily pull in the freshly generated class together with the runtime wrappers + // and the referenced `User` entity. Importing the wrappers/User through the same + // specifiers the generated module uses keeps the module instances identical, so + // `instanceof MArray` / `instanceof User` checks line up with the generated code. + const loadScenario = async () => { + const [{ Anonymouse }, ops, { User }] = await Promise.all([ + import(scenarioOutput), + import("./sdk/common/operators"), + import("./User"), + ]); + + return { Anonymouse, User, ...ops }; + }; + + describe("string <-> instance conversion and M* operator scenarios", () => { + it("hydrates every wrapped field from a JSON string", async () => { + const { Anonymouse, MArray, MCollection, MOne, User } = + await loadScenario(); + + const json = JSON.stringify({ + arrayField: [{}, {}], + collectionField: [{}, {}, {}], + oneField: {}, + }); + + const instance = new Anonymouse(json); + + expect(instance).toBeInstanceOf(Anonymouse); + + // array -> MArray of inline ArrayField DTOs + expect(instance.arrayField).toBeInstanceOf(MArray); + expect(instance.arrayField.isReplace()).toBe(true); + expect(instance.arrayField.len()).toBe(2); + expect(instance.arrayField.get()[0]).toBeInstanceOf(Anonymouse.ArrayField); + + // collection -> MCollection of the target entity (User) + expect(instance.collectionField).toBeInstanceOf(MCollection); + expect(instance.collectionField.len()).toBe(3); + expect( + instance.collectionField.get().every((u: unknown) => u instanceof User), + ).toBe(true); + + // one -> MOne wrapping a User + expect(instance.oneField).toBeInstanceOf(MOne); + expect(instance.oneField.get()).toBeInstanceOf(User); + }); + + it("round-trips object -> string -> object without drift", async () => { + const { Anonymouse } = await loadScenario(); + + const original = new Anonymouse({ + arrayField: [{}, {}], + collectionField: [{}], + oneField: {}, + }); + + const serialised = original.toString(); + expect(typeof serialised).toBe("string"); + + const revived = new Anonymouse(serialised); + expect(revived).toBeInstanceOf(Anonymouse); + + // A second hop must produce byte-identical JSON — the wire form is stable. + expect(JSON.parse(revived.toString())).toEqual(JSON.parse(serialised)); + }); + + it("serialises a replace array as a bare list and an append array as a tagged object", async () => { + const { Anonymouse, MArray } = await loadScenario(); + + // replace is implicit on the wire — a bare array. + const replace = new Anonymouse(); + replace.arrayField = [ + new Anonymouse.ArrayField(), + new Anonymouse.ArrayField(), + ]; + expect(replace.arrayField.isReplace()).toBe(true); + + const replaceWire = JSON.parse(replace.toString()).arrayField; + expect(Array.isArray(replaceWire)).toBe(true); + expect(replaceWire).toHaveLength(2); + + // append is tagged so the reader keeps the existing rows. + const append = new Anonymouse(); + append.arrayField = MArray.append([new Anonymouse.ArrayField()]); + expect(append.arrayField.isAppend()).toBe(true); + + const appendWire = JSON.parse(append.toString()).arrayField; + expect(Array.isArray(appendWire)).toBe(false); + expect(appendWire).toMatchObject({ __operation: "append" }); + expect(appendWire.items).toHaveLength(1); + }); + + it("casts plain objects in a collection into User instances and preserves a tagged append", async () => { + const { Anonymouse, MCollection, User } = await loadScenario(); + + const replace = new Anonymouse({ collectionField: [{}, {}] }); + expect(replace.collectionField.isReplace()).toBe(true); + expect( + replace.collectionField.get().every((u: unknown) => u instanceof User), + ).toBe(true); + + // A tagged append object handed straight to the constructor is recognised + // through MCollection.cast and keeps its "append" operation. + const appended = new Anonymouse({ + collectionField: { __operation: "append", items: [{}, {}, {}] }, + }); + expect(appended.collectionField).toBeInstanceOf(MCollection); + expect(appended.collectionField.isAppend()).toBe(true); + expect(appended.collectionField.len()).toBe(3); + }); + + it("wraps a one field regardless of the input shape", async () => { + const { Anonymouse, MOne, User } = await loadScenario(); + + // plain object + const fromPlain = new Anonymouse({ oneField: {} }); + expect(fromPlain.oneField).toBeInstanceOf(MOne); + expect(fromPlain.oneField.get()).toBeInstanceOf(User); + + // already a User instance + const fromUser = new Anonymouse(); + fromUser.oneField = new User({}); + expect(fromUser.oneField).toBeInstanceOf(MOne); + expect(fromUser.oneField.get()).toBeInstanceOf(User); + + // already an MOne wrapper — assigned through untouched + const wrapper = MOne.of(new User({})); + const fromWrapper = new Anonymouse(); + fromWrapper.oneField = wrapper; + expect(fromWrapper.oneField).toBe(wrapper); + }); + + it("emits a selector wire form for MOne.select", async () => { + const { MOne } = await loadScenario(); + + const selector = MOne.select({ id: 42 }); + expect(selector.isSelector()).toBe(true); + + // A selector serialises to the explicit replace form rather than content. + expect(JSON.parse(JSON.stringify(selector))).toEqual({ + __operation: "replace", + selector: { id: 42 }, + }); + }); + + it("setters accept both the M* wrapper form and the pure value form", async () => { + const { Anonymouse, MArray, MCollection, MOne, User } = + await loadScenario(); + + const instance = new Anonymouse(); + + // pure value forms — setters return `this` for chaining. + expect(instance.setArrayField([new Anonymouse.ArrayField()])).toBe( + instance, + ); + expect(instance.arrayField).toBeInstanceOf(MArray); + expect(instance.arrayField.isReplace()).toBe(true); + + expect(instance.setCollectionField([{}])).toBe(instance); + expect(instance.collectionField.get()[0]).toBeInstanceOf(User); + + expect(instance.setOneField(new User({}))).toBe(instance); + expect(instance.oneField.get()).toBeInstanceOf(User); + + // wrapper forms — assigned through while keeping their operation. + instance.setArrayField(MArray.append([new Anonymouse.ArrayField()])); + expect(instance.arrayField.isAppend()).toBe(true); + + instance.setCollectionField(MCollection.append([new User({})])); + expect(instance.collectionField.isAppend()).toBe(true); + + instance.setOneField(MOne.of(new User({}))); + expect(instance.oneField).toBeInstanceOf(MOne); + }); + + it("leaves nullable fields undefined when never supplied", async () => { + const { Anonymouse } = await loadScenario(); + + const instance = new Anonymouse({}); + expect(instance.collectionNullableField).toBeUndefined(); + expect(instance.oneNullableField).toBeUndefined(); + + // ...and they are simply omitted from the serialised wire form. + const wire = JSON.parse(instance.toString()); + expect("collectionNullableField" in wire).toBe(false); + expect("oneNullableField" in wire).toBe(false); + }); + + it("accepts an explicit null for a nullable collection", async () => { + const { Anonymouse } = await loadScenario(); + + const instance = new Anonymouse(); + instance.collectionNullableField = null; + expect(instance.collectionNullableField).toBeNull(); + }); + }); +}); diff --git a/examples/js/cases/emi-module-enums.test.ts b/examples/js/cases/emi-module-enums.test.ts index b436d1de..14e4f0b7 100644 --- a/examples/js/cases/emi-module-enums.test.ts +++ b/examples/js/cases/emi-module-enums.test.ts @@ -37,12 +37,10 @@ sidebar: order: 4 --- - - Besides defining inline enums, emi definition allows for standalone enums which is really useful, for sharing enums between multiple files. - + \`\`\`yaml ${yaml.dump(sample1)} \`\`\` diff --git a/examples/js/cases/emi-object-auto-init.test.ts b/examples/js/cases/emi-object-auto-init.test.ts index 69e9390c..44d49529 100644 --- a/examples/js/cases/emi-object-auto-init.test.ts +++ b/examples/js/cases/emi-object-auto-init.test.ts @@ -4,6 +4,7 @@ import { describe, expect, it } from "vitest"; import { createInstance } from "../../emi-wasm-helper/getPublicActions"; import yaml from "js-yaml"; import { AutoInitClassDto } from "../test-artifacts/auto-init.dto"; +import { MArray, MCollection } from "../test-artifacts/sdk/common/operators"; describe("Objects auto init should work perfectly fine.", () => { const content: string[] = []; @@ -105,33 +106,36 @@ Emi compiler generates ts type and, class, with full getter, setters and validat let m = new AutoInitClassDto(); // By default, the contents need to be an array, regardless. - expect(m.object1.object2.contacts).to.be.an("array"); + expect(m.object1.object2.contacts).to.be.instanceOf(MArray); m = AutoInitClassDto.from({ object1: { object2: { contacts: [] } } }); - expect(m.object1.object2.contacts).to.be.an("array"); + expect(m.object1.object2.contacts).to.be.instanceOf(MArray); // Should not be able to set 22 as an array! m = AutoInitClassDto.from({ object1: { object2: { contacts: 22 as any } }, }); - expect(m.object1.object2.contacts).to.be.an("array"); + + expect(m.object1.object2.contacts).to.be.instanceOf(MArray); m = AutoInitClassDto.from({ object1: { object2: { contacts: [{ email: "hass" }] } }, }); - expect(m.object1.object2.contacts[0].email).toEqual("hass"); + expect(m.object1.object2.contacts.get()[0].email).toEqual("hass"); m = AutoInitClassDto.from({ object1: { object2: { contacts: [{} as any] } }, }); - expect(m.object1.object2.contacts[0].email).toEqual( + expect(m.object1.object2.contacts.get()[0].email).toEqual( "emi-compiler@emi-compiler.com", ); // Phone needs to be exactly as undefined. - expect(typeof m.object1.object2.contacts[0].phone).toEqual("undefined"); + expect(typeof m.object1.object2.contacts.get()[0].phone).toEqual( + "undefined", + ); }); /// Last step is to write the document down diff --git a/examples/js/cases/scenarios.output.js b/examples/js/cases/scenarios.output.js new file mode 100644 index 00000000..fd017c36 --- /dev/null +++ b/examples/js/cases/scenarios.output.js @@ -0,0 +1,431 @@ +import { MArray, MCollection, MOne } from './sdk/common/operators'; +import { User } from './User'; +import { withPrefix } from './sdk/common/withPrefix'; +/** + * The base class definition for anonymouse + **/ +export class Anonymouse { + /** + * array field, non-nullable + * @type {Anonymouse.ArrayField} + **/ + #arrayField = MArray.of([]) + /** + * array field, non-nullable + * @returns {Anonymouse.ArrayField} + **/ +get arrayField () { return this.#arrayField } +/** + * array field, non-nullable + * @type {Anonymouse.ArrayField} + **/ +set arrayField (value) { + // When the passed value is already an array, we check if we need to + // cast the inner items into class instance. + if (Array.isArray(value)) { + if (value.length > 0 && value[0] instanceof Anonymouse.ArrayField) { + this.#arrayField = MArray.of(value); + } else { + this.#arrayField = MArray.of( + value.map((item) => new Anonymouse.ArrayField(item)), + ); + } + return; + } + // If the instance is already an MArray, we assume it's all good. + if (value instanceof MArray) { + this.#arrayField = value; + return; + } + // If the value is not array, and is not a MArray, we need to be consider, + // it might be eligible to be casted into MArray. + const { ok, value: mcastValue } = MArray.cast(value); + if (ok) { + this.#arrayField = mcastValue; + return; + } + console.warn( + "Cannot assing value to arrayField, because it needs MArray instance or an Array.", + ); +} +setArrayField (value) { + this.arrayField = value + return this +} + /** + * arrayNullable field, non-nullable + * @type {any} + **/ + #arrayNullableField + /** + * arrayNullable field, non-nullable + * @returns {any} + **/ +get arrayNullableField () { return this.#arrayNullableField } +/** + * arrayNullable field, non-nullable + * @type {any} + **/ +set arrayNullableField (value) { + this.#arrayNullableField = value; +} +setArrayNullableField (value) { + this.arrayNullableField = value + return this +} + /** + * collection field, non-nullable + * @type {User[]} + **/ + #collectionField = MCollection.of([]) + /** + * collection field, non-nullable + * @returns {User[]} + **/ +get collectionField () { return this.#collectionField } +/** + * collection field, non-nullable + * @type {User[]} + **/ +set collectionField (value) { + // When the passed value is already an array, we check if we need to + // cast the inner items into class instance. + if (Array.isArray(value)) { + if (value.length > 0 && value[0] instanceof User) { + this.#collectionField = MCollection.of(value); + } else { + this.#collectionField = MCollection.of( + value.map((item) => new User(item)), + ); + } + return; + } + // If the instance is already an MCollection, we assume it's all good. + if (value instanceof MCollection) { + this.#collectionField = value; + return; + } + // If the value is not array, and is not a MCollection, we need to be consider, + // it might be eligible to be casted into MCollection. + const { ok, value: mcastValue } = MCollection.cast(value); + if (ok) { + this.#collectionField = mcastValue; + return; + } + console.warn( + "Cannot assing value to collectionField, because it needs MCollection instance or an Array.", + ); +} +setCollectionField (value) { + this.collectionField = value + return this +} + /** + * collectionNullable field, non-nullable + * @type {User[]} + **/ + #collectionNullableField = undefined + /** + * collectionNullable field, non-nullable + * @returns {User[]} + **/ +get collectionNullableField () { return this.#collectionNullableField } +/** + * collectionNullable field, non-nullable + * @type {User[]} + **/ +set collectionNullableField (value) { + // For nullable collection, we allow explicit undefined or null values + if (value === null || value === undefined) { + this.#collectionNullableField = value; + return + } + // When the passed value is already an array, we check if we need to + // cast the inner items into class instance. + if (Array.isArray(value)) { + if (value.length > 0 && value[0] instanceof User) { + this.#collectionNullableField = MCollection.of(value); + } else { + this.#collectionNullableField = MCollection.of( + value.map((item) => new User(item)), + ); + } + return; + } + // If the instance is already an MCollection, we assume it's all good. + if (value instanceof MCollection) { + this.#collectionNullableField = value; + return; + } + // If the value is not array, and is not a MCollection, we need to be consider, + // it might be eligible to be casted into MCollection. + const { ok, value: mcastValue } = MCollection.cast(value); + if (ok) { + this.#collectionNullableField = mcastValue; + return; + } + console.warn( + "Cannot assing value to collectionNullableField, because it needs MCollection instance or an Array.", + ); +} +setCollectionNullableField (value) { + this.collectionNullableField = value + return this +} + /** + * one field, non-nullable + * @type {User} + **/ + #oneField + /** + * one field, non-nullable + * @returns {User} + **/ +get oneField () { return this.#oneField } +/** + * one field, non-nullable + * @type {User} + **/ +set oneField (value) { + // For objects, the sub type needs to always be instance of the sub class. + if (value instanceof MOne) { + this.#oneField = value + } else if (value instanceof User) { + this.#oneField = MOne.of(value) + } else { + this.#oneField = MOne.of(new User(value)) + } +} +setOneField (value) { + this.oneField = value + return this +} + /** + * oneNullable field, non-nullable + * @type {User} + **/ + #oneNullableField = undefined + /** + * oneNullable field, non-nullable + * @returns {User} + **/ +get oneNullableField () { return this.#oneNullableField } +/** + * oneNullable field, non-nullable + * @type {User} + **/ +set oneNullableField (value) { + // For objects, the sub type needs to always be instance of the sub class. + if (value instanceof MOne) { + this.#oneNullableField = value + } else if (value instanceof User) { + this.#oneNullableField = MOne.of(value) + } else { + this.#oneNullableField = MOne.of(new User(value)) + } +} +setOneNullableField (value) { + this.oneNullableField = value + return this +} +/** + * The base class definition for arrayField + **/ +static ArrayField = class ArrayField { + constructor(data) { + if (data === null || data === undefined) { + return; + } + if (typeof data === "string") { + this.applyFromObject(JSON.parse(data)); + } else if (this.#isJsonAppliable(data)) { + this.applyFromObject(data); + } else { + throw new Error("Instance cannot be created on an unknown value, check the content being passed. got: " + typeof data); + } + } + #isJsonAppliable(obj) { + const g = globalThis + const isBuffer = + typeof g.Buffer !== "undefined" && + typeof g.Buffer.isBuffer === "function" && + g.Buffer.isBuffer(obj); + const isBlob = + typeof g.Blob !== "undefined" && obj instanceof g.Blob; + return ( + obj && + typeof obj === "object" && + !Array.isArray(obj) && + !isBuffer && + !(obj instanceof ArrayBuffer) && + !isBlob + ); + } + /** + * casts the fields of a javascript object into the class properties one by one + **/ + applyFromObject(data = {}) { + const d = data; + } + /** + * Special toJSON override, since the field are private, + * Json stringify won't see them unless we mention it explicitly. + **/ + toJSON() { + return { + }; + } + toString() { + return JSON.stringify(this); + } + static get Fields() { + return { + } + } + /** + * Creates an instance of Anonymouse.ArrayField, and possibleDtoObject + * needs to satisfy the type requirement fully, otherwise typescript compile would + * be complaining. + **/ + static from(possibleDtoObject) { + return new Anonymouse.ArrayField(possibleDtoObject); + } + /** + * Creates an instance of Anonymouse.ArrayField, and partialDtoObject + * needs to satisfy the type, but partially, and rest of the content would + * be constructed according to data types and nullability. + **/ + static with(partialDtoObject) { + return new Anonymouse.ArrayField(partialDtoObject); + } + copyWith(partial) { + return new Anonymouse.ArrayField ({ ...this.toJSON(), ...partial }); + } + clone() { + return new Anonymouse.ArrayField(this.toJSON()); + } +} + constructor(data) { + if (data === null || data === undefined) { + this.#lateInitFields(); + return; + } + if (typeof data === "string") { + this.applyFromObject(JSON.parse(data)); + } else if (this.#isJsonAppliable(data)) { + this.applyFromObject(data); + } else { + throw new Error("Instance cannot be created on an unknown value, check the content being passed. got: " + typeof data); + } + } + #isJsonAppliable(obj) { + const g = globalThis + const isBuffer = + typeof g.Buffer !== "undefined" && + typeof g.Buffer.isBuffer === "function" && + g.Buffer.isBuffer(obj); + const isBlob = + typeof g.Blob !== "undefined" && obj instanceof g.Blob; + return ( + obj && + typeof obj === "object" && + !Array.isArray(obj) && + !isBuffer && + !(obj instanceof ArrayBuffer) && + !isBlob + ); + } + /** + * casts the fields of a javascript object into the class properties one by one + **/ + applyFromObject(data = {}) { + const d = data; + if (d.arrayField !== undefined) { this.arrayField = d.arrayField } + if (d.arrayNullableField !== undefined) { this.arrayNullableField = d.arrayNullableField } + if (d.collectionField !== undefined) { this.collectionField = d.collectionField } + if (d.collectionNullableField !== undefined) { this.collectionNullableField = d.collectionNullableField } + if (d.oneField !== undefined) { this.oneField = d.oneField } + if (d.oneNullableField !== undefined) { this.oneNullableField = d.oneNullableField } + this.#lateInitFields(data) + } + /** + * These are the class instances, which need to be initialised, regardless of the constructor incoming data + **/ + #lateInitFields(data = {}) { + const d = data; + if (!(d.oneField instanceof User)) { this.oneField = MOne.of(new User(d.oneField || {})) } + } + /** + * Special toJSON override, since the field are private, + * Json stringify won't see them unless we mention it explicitly. + **/ + toJSON() { + return { + arrayField: this.#arrayField, + arrayNullableField: this.#arrayNullableField, + collectionField: this.#collectionField, + collectionNullableField: this.#collectionNullableField, + oneField: this.#oneField, + oneNullableField: this.#oneNullableField, + }; + } + toString() { + return JSON.stringify(this); + } + static get Fields() { + return { + arrayField$: 'arrayField', +get arrayField() { + return withPrefix( + "arrayField[:i]", + Anonymouse.ArrayField.Fields + ); + }, + arrayNullableField: 'arrayNullableField', + collectionField$: 'collectionField', +get collectionField() { + return withPrefix( + "collectionField[:i]", + User.Fields + ); + }, + collectionNullableField$: 'collectionNullableField', +get collectionNullableField() { + return withPrefix( + "collectionNullableField", + User.Fields + ); + }, + oneField$: 'oneField', +get oneField() { + return withPrefix( + "oneField", + User.Fields + ); + }, + oneNullableField: 'oneNullableField', + } + } + /** + * Creates an instance of Anonymouse, and possibleDtoObject + * needs to satisfy the type requirement fully, otherwise typescript compile would + * be complaining. + **/ + static from(possibleDtoObject) { + return new Anonymouse(possibleDtoObject); + } + /** + * Creates an instance of Anonymouse, and partialDtoObject + * needs to satisfy the type, but partially, and rest of the content would + * be constructed according to data types and nullability. + **/ + static with(partialDtoObject) { + return new Anonymouse(partialDtoObject); + } + copyWith(partial) { + return new Anonymouse ({ ...this.toJSON(), ...partial }); + } + clone() { + return new Anonymouse(this.toJSON()); + } +} \ No newline at end of file diff --git a/examples/js/cases/sdk/common/URLSearchParamsX.ts b/examples/js/cases/sdk/common/URLSearchParamsX.ts new file mode 100644 index 00000000..df1f8e4e --- /dev/null +++ b/examples/js/cases/sdk/common/URLSearchParamsX.ts @@ -0,0 +1,144 @@ +import { stringify, parse } from "qs"; + +/** + * Extended URLSearchParams that stores data in a nested object + * and keeps compatibility with URLSearchParams methods. + */ +export class URLSearchParamsX extends URLSearchParams { + /** Internal data store */ + private data: Record = {}; + + constructor( + init?: string[][] | Record | string | URLSearchParams + ) { + super(init); + if (init) { + if (typeof init === "string") { + Object.assign(this.data, parse(init)); + } else if (init instanceof URLSearchParams) { + Object.assign(this.data, parse(init.toString())); + } else if (Array.isArray(init)) { + init.forEach(([k, v]) => (this.data[k] = v)); + } else { + Object.assign(this.data, init); + } + } + } + + /** Remove a key from the store */ + override delete(name: string): void { + delete this.data[name]; + } + + /** Append a value to an array or create a new array */ + override append(name: string, value: string): void { + if (this.data[name] === undefined) this.data[name] = value; + else if (Array.isArray(this.data[name])) this.data[name].push(value); + else this.data[name] = [this.data[name], value]; + } + + /** Get an iterator of top-level keys */ + override keys(): URLSearchParamsIterator { + const obj = this.data; + return (function* (): Generator { + for (const key of Object.keys(obj)) { + yield key; + } + return undefined; + })(); + } + + /** Number of top-level keys */ + override get size(): number { + return Object.keys(this.data).length; + } + + /** Sort top-level keys */ + override sort(): void { + const sorted: Record = {}; + Object.keys(this.data) + .sort() + .forEach((key) => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + sorted[key] = this.data[key]; + }); + this.data = sorted; + } + + /** Get an iterator of top-level values */ + override values(): URLSearchParamsIterator { + const obj = this.data; + return (function* (): Generator { + for (const key of Object.keys(obj)) { + const val = obj[key]; + // Make sure val is string + yield String(val); + } + return undefined; + })(); + } + + /** Get a single value by key */ + override get(name: string): string | null { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const val = this.data[name]; + if (val == null) return null; + return Array.isArray(val) ? String(val[0]) : String(val); + } + + /** Check if key exists */ + override has(name: string): boolean { + return this.data[name] !== undefined; + } + + /** Iterate over top-level keys and values */ + override forEach( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + callbackfn: (value: any, key: string, parent: any) => void + ): void { + for (const key of Object.keys(this.data)) { + callbackfn(this.data[key], key, this); + } + } + + /** Get all values for a key as array */ + override getAll(name: string): string[] { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const val = this.data[name]; + if (val === undefined) return []; + return Array.isArray(val) ? val.map(String) : [String(val)]; + } + + /** Get an iterator of key/value pairs (flattened) */ + override entries() { + const params = new URLSearchParams(stringify(this.data)); + return params.entries(); + } + + /** Convert to query string */ + override toString(): string { + return stringify(this.data); + } + + /** Set a key to a value */ + override set(name: string, value: any): this { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + this.data[name] = value; + return this; + } + + /** Convert entries to plain object */ + toObject(): Record { + return Object.fromEntries(this.entries()); + } + + // eslint-disable-next-line no-unused-private-class-members + protected getTyped(key: string, type: string) { + const val = this.get(key); + if (val == null) return null; + const t = type.toLowerCase(); + if (t.includes("number")) return Number(val); + if (t.includes("bool")) return val === "true"; + return val; + } +} diff --git a/examples/js/cases/sdk/common/WebSocketX.ts b/examples/js/cases/sdk/common/WebSocketX.ts new file mode 100644 index 00000000..cb1d758f --- /dev/null +++ b/examples/js/cases/sdk/common/WebSocketX.ts @@ -0,0 +1,97 @@ +type ConstructorWithArg = new (arg: T, ...rest: any[]) => R; + +export class WebSocketX< + SendType = string | ArrayBufferLike | Blob | ArrayBufferView, + RecieveData = string +> extends WebSocket { + public readonly addEventListenerRaw: WebSocket["addEventListener"]; + public readonly sendRaw: WebSocket["send"]; + #factoryCls?: ConstructorWithArg; + + constructor( + url: string | URL, + protocols?: string | string[], + options?: { + MessageFactoryClass?: ConstructorWithArg; + } + ) { + super(url, protocols); + + this.sendRaw = super.send.bind(this); + this.addEventListenerRaw = super.addEventListener.bind(this); + + if (options?.MessageFactoryClass) { + this.#factoryCls = options.MessageFactoryClass; + } + } + + set onmessage( + fn: ((this: WebSocket, ev: MessageEvent) => any) | null + ) { + if (fn) { + this.addEventListener("message", fn); + } else { + super.onmessage = null; + } + } + + get onmessage() { + return super.onmessage; + } + + // @ts-expect-error override to customize send + send(data: SendType): void { + if ( + typeof data === "string" || + data instanceof Blob || + data instanceof ArrayBuffer || + ArrayBuffer.isView(data) + ) { + super.send(data); + } else if (data !== undefined && data !== null) { + super.send(data.toString()); + } + } + + addEventListener( + type: "message", + listener: (this: WebSocket, ev: MessageEvent) => unknown, + options?: boolean | AddEventListenerOptions + ): void; + + // fallback overloads (other event types) + addEventListener>( + type: K, + listener: (this: WebSocket, ev: WebSocketEventMap[K]) => unknown, + options?: boolean | AddEventListenerOptions + ): void; + + // implementation + addEventListener( + type: string, + listener: EventListenerOrEventListenerObject, + options?: boolean | AddEventListenerOptions + ): void { + if (type === "message") { + const wrapped = ((ev: MessageEvent) => { + let parsed: unknown = ev.data; + if (this.#factoryCls) { + try { + parsed = new this.#factoryCls(ev.data); + } catch { + // if constructor rejects (e.g. ArrayBuffer not supported), keep raw + } + } + + (listener as EventListener).call( + this, + new MessageEvent("message", { data: parsed }) + ); + }) as EventListener; + + super.addEventListener(type, wrapped, options); + } else { + super.addEventListener(type, listener, options); + } + } +} diff --git a/examples/js/cases/sdk/common/buildUrl.ts b/examples/js/cases/sdk/common/buildUrl.ts new file mode 100644 index 00000000..cf08f1cc --- /dev/null +++ b/examples/js/cases/sdk/common/buildUrl.ts @@ -0,0 +1,34 @@ +/** + * Handy tool to create a final callable url, from query string, query params, + * and the actual url. + * @param template + * @param params + * @param qs + * @returns + */ +export function buildUrl( + url: string, + params?: Record, + qs?: URLSearchParams +) { + // Replace :placeholders + if (params) { + Object.entries(params as Record).forEach(([key, value]) => { + url = url.replace( + new RegExp(`:${key}`, "g"), + encodeURIComponent(String(value)) + ); + }); + } + + if (qs && qs instanceof URLSearchParams) { + url += `?${qs.toString()}`; + } else if (qs && Object.keys(qs).length) { + const query = new URLSearchParams( + Object.entries(qs).map(([k, v]) => [k, String(v)]) + ).toString(); + url += `?${query}`; + } + + return url; +} diff --git a/examples/js/cases/sdk/common/fetchx.ts b/examples/js/cases/sdk/common/fetchx.ts new file mode 100644 index 00000000..774f16b0 --- /dev/null +++ b/examples/js/cases/sdk/common/fetchx.ts @@ -0,0 +1,196 @@ +export type TypedRequestInit = Omit< + RequestInit, + "body" | "headers" +> & { + body?: TBody; + headers?: THeaders; +}; +export class TypedResponse extends Response { + override json(): Promise { + return super.json(); + } + result: + | T + | undefined + | ReadableStream> + | null + | string; +} +export async function fetchx< + TResponse = unknown, + TBody = unknown, + THeaders = unknown, +>( + input: RequestInfo | URL, + init?: TypedRequestInit, + ctx?: FetchxContext | null, +): Promise> { + let url = input.toString(); + let reqInit: TypedRequestInit = init || {}; + let res: TypedResponse; + let fetchFn = fetch; + + if (ctx) { + [url, reqInit] = await ctx.apply(url, reqInit); + + if (ctx.fetchOverrideFn) { + fetchFn = ctx.fetchOverrideFn; + } + } + + res = (await fetchFn( + url, + reqInit as RequestInit, + )) as TypedResponse; + + if (ctx) { + res = await ctx.handle(res); + } + return res; +} +type DtoFactory = { new (data: any): T } | ((data: any) => T); +function isConstructor(fn: DtoFactory): fn is { new (data: any): T } { + return ( + typeof fn === "function" && fn.prototype && fn.prototype.constructor === fn + ); +} +export async function handleFetchResponse( + res: TypedResponse, + dto?: DtoFactory, + onMessage?: (msg: any) => void, + signal?: AbortSignal | null, +): Promise<{ done: Promise; response: TypedResponse }> { + const ct = res.headers.get("content-type") || ""; + const cd = res.headers.get("content-disposition") || ""; + if (ct.includes("text/event-stream")) { + return SSEFetch(res, onMessage, signal); + } + if ( + cd.includes("attachment") || + (!ct.includes("json") && !ct.startsWith("text/")) + ) { + (res as any).result = res.body; + } else if (ct.includes("application/json")) { + const json = await res.json(); + if (dto) { + if (isConstructor(dto)) { + (res as any).result = new dto(json); // ✅ class constructor + } else { + (res as any).result = dto(json); // ✅ factory function + } + } else { + (res as any).result = json; + } + } else { + (res as any).result = await res.text(); + } + return { done: Promise.resolve(), response: res as any }; +} +export const SSEFetch = ( + res: TypedResponse, + onMessage?: (ev: MessageEvent) => void, + signal?: AbortSignal | null, +): { response: TypedResponse; done: Promise } => { + if (!res.body) throw new Error("SSE requires readable body"); + const reader = res.body.getReader(); + const decoder = new TextDecoder(); + let buffer = ""; + const done = new Promise((resolve, reject) => { + function readChunk() { + reader + .read() + .then(({ done: finished, value }) => { + if (signal?.aborted) { + reader.cancel(); + return resolve(); // resolve on abort + } + if (finished) return resolve(); // normal end + buffer += decoder.decode(value, { stream: true }); + const parts = buffer.split("\n\n"); + buffer = parts.pop() || ""; + for (const part of parts) { + let data = ""; + let event = "message"; + part.split("\n").forEach((line) => { + if (line.startsWith("data:")) data += line.slice(5).trim(); + else if (line.startsWith("event:")) event = line.slice(6).trim(); + }); + if (data) { + if (data === "[DONE]") return resolve(); + onMessage?.(new MessageEvent(event, { data })); + } + } + readChunk(); + }) + .catch((err) => { + if (err.name === "AbortError") resolve(); + else reject(err); + }); + } + readChunk(); + }); + return { response: res, done }; +}; +export class FetchxContext { + constructor( + public baseUrl: string = "", + public defaultHeaders: Record = {}, + public requestInterceptor?: ( + url: string, + init: TypedRequestInit, + ) => + | Promise<[string, TypedRequestInit]> + | [string, TypedRequestInit], + public responseInterceptor?: ( + res: TypedResponse, + ) => Promise>, + /** + * Overrides the browser fetch function, for different purposes. It would recieve the same first 2 arguments as fetch, + * as well as third one of fetchx context. If you pass the fetch itself to override, it should have no effect. + */ + public fetchOverrideFn?: ( + input: RequestInfo | URL, + init?: TypedRequestInit, + ) => Promise, + ) {} + async apply( + url: string, + init: TypedRequestInit, + ): Promise<[string, TypedRequestInit]> { + // prefix baseUrl + if (!/^https?:\/\//.test(url)) { + url = this.baseUrl + url; + } + // merge default headers + (init.headers as unknown) = { + ...this.defaultHeaders, + ...((init.headers as object) || {}), + }; + // call request interceptor if present + if (this.requestInterceptor) { + return this.requestInterceptor(url, init); + } + return [url, init]; + } + async handle(res: TypedResponse): Promise> { + if (this.responseInterceptor) { + return this.responseInterceptor(res); + } + return res; + } + clone(overrides?: Partial): FetchxContext { + return new FetchxContext( + overrides?.baseUrl ?? this.baseUrl, + { ...this.defaultHeaders, ...(overrides?.defaultHeaders || {}) }, + overrides?.requestInterceptor ?? this.requestInterceptor, + overrides?.responseInterceptor ?? this.responseInterceptor, + ); + } +} +export type PartialDeep = { + [P in keyof T]?: T[P] extends Array + ? Array> + : T[P] extends object + ? PartialDeep + : T[P]; +}; \ No newline at end of file diff --git a/examples/js/cases/sdk/common/isPlausibleObject.ts b/examples/js/cases/sdk/common/isPlausibleObject.ts new file mode 100644 index 00000000..e3f57236 --- /dev/null +++ b/examples/js/cases/sdk/common/isPlausibleObject.ts @@ -0,0 +1,18 @@ +export const isPlausibleObject = (obj: any) => { + const isBuffer = + typeof globalThis.Buffer !== "undefined" && + typeof globalThis.Buffer.isBuffer === "function" && + globalThis.Buffer.isBuffer(obj); + + const isBlob = + typeof globalThis.Blob !== "undefined" && obj instanceof globalThis.Blob; + + return ( + obj && + typeof obj === "object" && + !Array.isArray(obj) && + !isBuffer && + !(obj instanceof ArrayBuffer) && + !isBlob + ); +}; diff --git a/examples/js/cases/sdk/common/operators.ts b/examples/js/cases/sdk/common/operators.ts new file mode 100644 index 00000000..31b7af27 --- /dev/null +++ b/examples/js/cases/sdk/common/operators.ts @@ -0,0 +1,211 @@ +export class MOne { + private operation: string | null = null; + private selector: S | undefined; + + private content: T | undefined = undefined; + + isNull(): boolean { + return this.content === null; + } + + isSelector(): boolean { + return this.selector !== undefined && this.operation !== null; + } + + get(): T { + return this.content!; + } + + static of(value: T) { + const one = new MOne(); + one.content = value; + + return one; + } + + static select(selector: any) { + const one = new MOne(); + one.selector = selector; + one.operation = "replace"; + + return one; + } + + toJSON() { + // When its explicit replace, it means that we need to pass a selector, so reader will + // be able to replace it via internal mechanism + if (this.operation === "replace") { + return { + __operation: this.operation, + selector: this.selector, + }; + } + + return this.content; + } +} + +// In javascript, nullability and undefined is already working perfectly fine. +// hence, maybe just giving back one is enough. +export class MOneNullable extends MOne {} + +// Array describes how an incoming list should be applied to an existing one, +// mirroring emigo.Array on the Go side. It lets a PATCH-style payload say +// "replace the whole set" versus "append to the existing set". +// +// On the wire a "replace" is implicit (a bare array), while an "append" is +// tagged with __operation so the reader keeps the existing rows. This keeps +// the payload identical to the Go client/backend generators. +export class MArray { + private operation: "replace" | "append" = "replace"; + private items: T[] = []; + + static cast( + value: unknown, + ): { ok: boolean; value: MArray | null } { + if (typeof value === "object" && (value as any)?.__operation) { + const res = MArray.of((value as any)?.items); + res.operation = (value as any)?.__operation; + + return { + ok: true, + value: res, + }; + } + + return { + value: null, + ok: false, + }; + } + + isAppend(): boolean { + return this.operation === "append"; + } + + isReplace(): boolean { + return this.operation === "replace"; + } + + len(): number { + return this.items.length; + } + + get(): T[] { + return this.items; + } + + // Full replacement — existing rows are cleared before these are applied. + static of(items: T[]) { + const arr = new MArray(); + arr.items = items; + arr.operation = "replace"; + + return arr; + } + + // Append — existing rows are preserved and these are added alongside them. + static append(items: T[]) { + const arr = new MArray(); + arr.items = items; + arr.operation = "append"; + + return arr; + } + + toJSON() { + // "replace" is implicit on the wire, so we emit a bare array. Only the + // "append" operation needs the explicit tagged-object form. + if (this.operation === "append") { + return { + __operation: this.operation, + items: this.items, + }; + } + + return this.items; + } +} + +// In javascript, nullability and undefined is already working perfectly fine. +// hence, just extending Array is enough. +export class MArrayNullable extends Array {} + +// Collection mirrors emigo.Collection on the Go side. Structurally it is the +// same as Array — a list carrying a "replace"/"append" operation — but it is a +// distinct field type: a collection holds a list of a target entity, whereas an +// array holds a list of an inline DTO. +export class MCollection { + private operation: "replace" | "append" = "replace"; + private items: T[] = []; + + isAppend(): boolean { + return this.operation === "append"; + } + + isReplace(): boolean { + return this.operation === "replace"; + } + + static cast( + value: unknown, + ): { ok: boolean; value: MCollection | null } { + if (typeof value === "object" && (value as any)?.__operation) { + const res = MCollection.of((value as any)?.items); + res.operation = (value as any)?.__operation; + + return { + ok: true, + value: res, + }; + } + + return { + value: null, + ok: false, + }; + } + + len(): number { + return this.items.length; + } + + get(): T[] { + return this.items; + } + + // Full replacement — existing rows are cleared before these are applied. + static of(items: T[]) { + const collection = new MCollection(); + collection.items = items; + collection.operation = "replace"; + + return collection; + } + + // Append — existing rows are preserved and these are added alongside them. + static append(items: T[]) { + const collection = new MCollection(); + collection.items = items; + collection.operation = "append"; + + return collection; + } + + toJSON() { + // "replace" is implicit on the wire, so we emit a bare array. Only the + // "append" operation needs the explicit tagged-object form. + if (this.operation === "append") { + return { + __operation: this.operation, + items: this.items, + }; + } + + return this.items; + } +} + +// In javascript, nullability and undefined is already working perfectly fine. +// hence, just extending Collection is enough. +export class MCollectionNullable extends MCollection {} diff --git a/examples/js/cases/sdk/common/withPrefix.ts b/examples/js/cases/sdk/common/withPrefix.ts new file mode 100644 index 00000000..8630562e --- /dev/null +++ b/examples/js/cases/sdk/common/withPrefix.ts @@ -0,0 +1,22 @@ +export function withPrefix>( + prefix: string, + fields: T +): T { + const out: Record = {}; + for (const [k, v] of Object.entries(fields)) { + if (typeof v === "string") { + out[k] = `${prefix}.${v}`; + } else if (typeof v === "object" && v !== null) { + out[k] = v; + } + } + return out as T; +} + +export function at(source: string, ...args: number[]): string { + args.forEach((item) => { + source = source.replace("[:i]", `[${item}]`); + }); + + return source; +} diff --git a/examples/js/cases/sdk/js/is-plausible-object.ts b/examples/js/cases/sdk/js/is-plausible-object.ts new file mode 100644 index 00000000..2fc6200d --- /dev/null +++ b/examples/js/cases/sdk/js/is-plausible-object.ts @@ -0,0 +1,25 @@ +/** + * Used in fetch context, to detect if the response is not a buffer + * or arraybufer, then create class instance of it. + * In such cases when response is blob, its better to pass the original classes to caller. + * @param obj + * @returns + */ +export const isPlausibleObject = (obj: any) => { + const isBuffer = + typeof globalThis.Buffer !== "undefined" && + typeof globalThis.Buffer.isBuffer === "function" && + globalThis.Buffer.isBuffer(obj); + + const isBlob = + typeof globalThis.Blob !== "undefined" && obj instanceof globalThis.Blob; + + return ( + obj && + typeof obj === "object" && + !Array.isArray(obj) && + !isBuffer && + !(obj instanceof ArrayBuffer) && + !isBlob + ); +}; diff --git a/examples/js/cases/sdk/react/useFetchx.ts b/examples/js/cases/sdk/react/useFetchx.ts new file mode 100644 index 00000000..58017a04 --- /dev/null +++ b/examples/js/cases/sdk/react/useFetchx.ts @@ -0,0 +1,10 @@ +import { createContext, useContext } from "react"; +import { FetchxContext } from "../common/fetchx"; + +const FetchxContextReact = createContext(null); + +export const FetchxProvider = FetchxContextReact.Provider; + +export function useFetchxContext(): FetchxContext | null { + return useContext(FetchxContextReact); +} diff --git a/examples/js/cases/sdk/react/useSse.ts b/examples/js/cases/sdk/react/useSse.ts new file mode 100644 index 00000000..576fddde --- /dev/null +++ b/examples/js/cases/sdk/react/useSse.ts @@ -0,0 +1,68 @@ +import { useEffect, useRef, useState } from "react"; +import type { TypedRequestInit } from "../common/fetchx"; + +export interface UseSSEResult { + messages: Array; + error?: Error | null; + cancel: () => void; + restart: () => void; +} + +type SseFetchFn = ( + onMessage?: (ev: MessageEvent) => void, + qs?: Q, + init?: TypedRequestInit, + overrideUrl?: string +) => Promise; + +export function useSse( + fetchFn: SseFetchFn, + props?: { + qs?: Q; + init?: TypedRequestInit; + overrideUrl?: string; + } +) { + const acRef = useRef(null); + + const [state, setState] = useState>>({ + messages: [], + }); + + const create = () => { + const ac = new AbortController(); + acRef.current = ac; + + setState({ messages: [], error: null }); // reset on new stream + + fetchFn( + (ev) => { + setState((value) => { + const next = ev.data as T; + if ((value.messages || []).includes(next)) return value; + return { ...value, messages: [...(value.messages || []), next] }; + }); + }, + props?.qs, + { ...(props?.init || {}), signal: ac.signal }, + props?.overrideUrl + ).catch((err) => setState((v) => ({ ...v, error: err as Error }))); + + return () => acRef.current?.abort(); + }; + + useEffect(() => { + return create(); + }, []); + + const cancel = () => { + acRef.current?.abort(); + }; + + const restart = () => { + cancel(); + create(); + }; + + return { ...state, cancel, restart, messages: state.messages as T[] }; +} diff --git a/examples/js/cases/sdk/react/useWebSocketX.ts b/examples/js/cases/sdk/react/useWebSocketX.ts new file mode 100644 index 00000000..ccdc434d --- /dev/null +++ b/examples/js/cases/sdk/react/useWebSocketX.ts @@ -0,0 +1,104 @@ +import { useEffect, useRef, useState, useCallback } from "react"; +import { WebSocketX } from "../common/WebSocketX"; + +export type UseWebSocketResult = { + send: (msg: TSend) => void; + close: () => void; + restart: () => void; + socket?: WebSocketX; +} & UseWebSocketState; + +interface UseWebSocketState { + messages: TRecv[]; + isOpen: boolean; + error?: Event | null; +} + +export function useWebSocketX( + fn: ( + overrideUrl?: string | undefined, + qs?: TQuery | undefined + ) => WebSocketX +): UseWebSocketResult { + const socketRef = useRef | null>(null); + const [state, setState] = useState>({ + messages: [], + isOpen: false, + error: undefined, + }); + + const create = useCallback(() => { + const ws = fn(); + socketRef.current = ws; + + setState({ + messages: [], + error: undefined, + isOpen: ws.readyState === ws.OPEN, + }); + + ws.addEventListener("message", (ev) => { + setState((prev) => { + return { + ...prev, + messages: [...prev.messages, ev.data], + }; + }); + }); + + ws.addEventListener("error", (ev) => { + setState((prev) => { + return { + ...prev, + error: ev, + }; + }); + }); + + ws.addEventListener("open", () => { + setState((prev) => { + return { + ...prev, + isOpen: true, + }; + }); + }); + + ws.addEventListener("close", () => { + setState((prev) => { + return { + ...prev, + isOpen: false, + }; + }); + }); + + return ws; + }, []); + + useEffect(() => { + const ws = create(); + return () => ws.close(); + }, [create]); + + const send = (msg: TSend) => { + socketRef.current?.send(msg); + }; + + const close = () => { + socketRef.current?.close(); + }; + + const restart = () => { + close(); + create(); + }; + + return { + ...state, + send, + close, + restart, + socket: socketRef.current || undefined, + }; +} diff --git a/examples/js/common.ts b/examples/js/common.ts index cedf34cc..541c0221 100644 --- a/examples/js/common.ts +++ b/examples/js/common.ts @@ -1,6 +1,6 @@ import { createInstance } from "../emi-wasm-helper/getPublicActions"; import yaml from "js-yaml"; -import fs from "node:fs"; +import fs, { writeFileSync } from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { @@ -91,7 +91,7 @@ function checkTsCode(code: string) { return { valid: false, errors }; } -function runEmiActionTs( +function runEmiActionTsNoCheck( actionWasmFunctionName, emiActionDefinition, context = {}, @@ -101,7 +101,18 @@ function runEmiActionTs( context, ); - // resp = resp.replace(/^import .*$/gm, ""); + return { resp }; +} + +function runEmiActionTs( + actionWasmFunctionName, + emiActionDefinition, + context = {}, +) { + let resp = globalThis[actionWasmFunctionName]( + toYaml(emiActionDefinition), + context, + ); resp += ` @@ -110,6 +121,7 @@ function runEmiActionTs( const validation = checkTsCode(resp); if (!validation.valid) { + writeFileSync("./last-failed-gen", resp); console.error(validation.errors); console.log(resp); throw validation.errors; @@ -167,6 +179,7 @@ export { parseGenerated, randomBetween, runEmiActionTs, + runEmiActionTsNoCheck, getJsDoc, createSocketServer, runSocketClientTest, diff --git a/examples/js/test-artifacts/auto-init.dto.ts b/examples/js/test-artifacts/auto-init.dto.ts index fc7d07e9..05aff3f3 100644 --- a/examples/js/test-artifacts/auto-init.dto.ts +++ b/examples/js/test-artifacts/auto-init.dto.ts @@ -1,11 +1,4 @@ -import { - MArray, - MArrayNullable, - MCollection, - MCollectionNullable, - MOne, - MOneNullable, -} from "./sdk/common/operators"; +import { MArray } from "./sdk/common/operators"; import { type PartialDeep } from "./sdk/common/fetchx"; import { withPrefix } from "./sdk/common/withPrefix"; /** @@ -80,9 +73,9 @@ export class AutoInitClassDto { * This field will be always an array * @type {AutoInitClassDto.Object1.Object2.Contacts} **/ - #contacts: InstanceType< - typeof AutoInitClassDto.Object1.Object2.Contacts - >[] = []; + #contacts: MArray< + InstanceType + > = MArray.of([]); /** * This field will be always an array * @returns {AutoInitClassDto.Object1.Object2.Contacts} @@ -95,24 +88,51 @@ export class AutoInitClassDto { * @type {AutoInitClassDto.Object1.Object2.Contacts} **/ set contacts( - value: InstanceType[], + value: + | MArray< + InstanceType + > + | InstanceType[], ) { - if (!Array.isArray(value) && !(value instanceof MCollection)) { + // When the passed value is already an array, we check if we need to + // cast the inner items into class instance. + if (Array.isArray(value)) { + if ( + value.length > 0 && + value[0] instanceof AutoInitClassDto.Object1.Object2.Contacts + ) { + this.#contacts = MArray.of(value); + } else { + this.#contacts = MArray.of( + value.map( + (item) => new AutoInitClassDto.Object1.Object2.Contacts(item), + ), + ); + } return; } - if ( - value.length > 0 && - value[0] instanceof AutoInitClassDto.Object1.Object2.Contacts - ) { + // If the instance is already an MArray, we assume it's all good. + if (value instanceof MArray) { this.#contacts = value; - } else { - this.#contacts = value.map( - (item) => new AutoInitClassDto.Object1.Object2.Contacts(item), - ); + return; } + // If the value is not array, and is not a MArray, we need to be consider, + // it might be eligible to be casted into MArray. + const { ok, value: mcastValue } = MArray.cast(value); + if (ok) { + this.#contacts = mcastValue as any; + return; + } + console.warn( + "Cannot assing value to contacts, because it needs MArray instance or an Array.", + ); } setContacts( - value: InstanceType[], + value: + | MArray< + InstanceType + > + | InstanceType[], ) { this.contacts = value; return this; diff --git a/examples/js/test-artifacts/http-action-test-output/HttpActionAction.ts b/examples/js/test-artifacts/http-action-test-output/HttpActionAction.ts index 07f7fb80..e51e6f0e 100644 --- a/examples/js/test-artifacts/http-action-test-output/HttpActionAction.ts +++ b/examples/js/test-artifacts/http-action-test-output/HttpActionAction.ts @@ -1,11 +1,3 @@ -import { - MArray, - MArrayNullable, - MCollection, - MCollectionNullable, - MOne, - MOneNullable, -} from "./sdk/common/operators"; import { buildUrl } from "./sdk/common/buildUrl"; import { fetchx, diff --git a/examples/js/test-artifacts/http-action-test-output/sdk/common/operators.ts b/examples/js/test-artifacts/http-action-test-output/sdk/common/operators.ts index df3b8e99..6ad63403 100644 --- a/examples/js/test-artifacts/http-action-test-output/sdk/common/operators.ts +++ b/examples/js/test-artifacts/http-action-test-output/sdk/common/operators.ts @@ -8,6 +8,23 @@ export class MOne { return this.content === null; } + static cast(value: unknown): { ok: boolean; value: MOne | null } { + if (typeof value === "object" && (value as any)?.__operation) { + const res = MOne.of((value as any)?.content); + res.operation = (value as any)?.__operation; + + return { + ok: true, + value: res, + }; + } + + return { + value: null, + ok: false, + }; + } + isSelector(): boolean { return this.selector !== undefined && this.operation !== null; } @@ -45,10 +62,6 @@ export class MOne { } } -// In javascript, nullability and undefined is already working perfectly fine. -// hence, maybe just giving back one is enough. -export class MOneNullable extends MOne {} - // Array describes how an incoming list should be applied to an existing one, // mirroring emigo.Array on the Go side. It lets a PATCH-style payload say // "replace the whole set" versus "append to the existing set". @@ -60,6 +73,25 @@ export class MArray { private operation: "replace" | "append" = "replace"; private items: T[] = []; + static cast( + value: unknown, + ): { ok: boolean; value: MArray | null } { + if (typeof value === "object" && (value as any)?.__operation) { + const res = MArray.of((value as any)?.items); + res.operation = (value as any)?.__operation; + + return { + ok: true, + value: res, + }; + } + + return { + value: null, + ok: false, + }; + } + isAppend(): boolean { return this.operation === "append"; } @@ -108,10 +140,6 @@ export class MArray { } } -// In javascript, nullability and undefined is already working perfectly fine. -// hence, just extending Array is enough. -export class MArrayNullable extends Array {} - // Collection mirrors emigo.Collection on the Go side. Structurally it is the // same as Array — a list carrying a "replace"/"append" operation — but it is a // distinct field type: a collection holds a list of a target entity, whereas an @@ -124,6 +152,25 @@ export class MCollection { return this.operation === "append"; } + static cast( + value: unknown, + ): { ok: boolean; value: MCollection | null } { + if (typeof value === "object" && (value as any)?.__operation) { + const res = MCollection.of((value as any)?.items); + res.operation = (value as any)?.__operation; + + return { + ok: true, + value: res, + }; + } + + return { + value: null, + ok: false, + }; + } + isReplace(): boolean { return this.operation === "replace"; } @@ -167,7 +214,3 @@ export class MCollection { return this.items; } } - -// In javascript, nullability and undefined is already working perfectly fine. -// hence, just extending Collection is enough. -export class MCollectionNullable extends MCollection {} diff --git a/examples/js/test-artifacts/http-emi-react-output/HttpActionAction.ts b/examples/js/test-artifacts/http-emi-react-output/HttpActionAction.ts index 64caf4c0..2b60da1f 100644 --- a/examples/js/test-artifacts/http-emi-react-output/HttpActionAction.ts +++ b/examples/js/test-artifacts/http-emi-react-output/HttpActionAction.ts @@ -1,12 +1,4 @@ import { GResponse } from "./sdk/envelopes/index"; -import { - MArray, - MArrayNullable, - MCollection, - MCollectionNullable, - MOne, - MOneNullable, -} from "./sdk/common/operators"; import { buildUrl } from "./sdk/common/buildUrl"; import { fetchx, diff --git a/examples/js/test-artifacts/http-emi-react-output/sdk/common/operators.ts b/examples/js/test-artifacts/http-emi-react-output/sdk/common/operators.ts index df3b8e99..6ad63403 100644 --- a/examples/js/test-artifacts/http-emi-react-output/sdk/common/operators.ts +++ b/examples/js/test-artifacts/http-emi-react-output/sdk/common/operators.ts @@ -8,6 +8,23 @@ export class MOne { return this.content === null; } + static cast(value: unknown): { ok: boolean; value: MOne | null } { + if (typeof value === "object" && (value as any)?.__operation) { + const res = MOne.of((value as any)?.content); + res.operation = (value as any)?.__operation; + + return { + ok: true, + value: res, + }; + } + + return { + value: null, + ok: false, + }; + } + isSelector(): boolean { return this.selector !== undefined && this.operation !== null; } @@ -45,10 +62,6 @@ export class MOne { } } -// In javascript, nullability and undefined is already working perfectly fine. -// hence, maybe just giving back one is enough. -export class MOneNullable extends MOne {} - // Array describes how an incoming list should be applied to an existing one, // mirroring emigo.Array on the Go side. It lets a PATCH-style payload say // "replace the whole set" versus "append to the existing set". @@ -60,6 +73,25 @@ export class MArray { private operation: "replace" | "append" = "replace"; private items: T[] = []; + static cast( + value: unknown, + ): { ok: boolean; value: MArray | null } { + if (typeof value === "object" && (value as any)?.__operation) { + const res = MArray.of((value as any)?.items); + res.operation = (value as any)?.__operation; + + return { + ok: true, + value: res, + }; + } + + return { + value: null, + ok: false, + }; + } + isAppend(): boolean { return this.operation === "append"; } @@ -108,10 +140,6 @@ export class MArray { } } -// In javascript, nullability and undefined is already working perfectly fine. -// hence, just extending Array is enough. -export class MArrayNullable extends Array {} - // Collection mirrors emigo.Collection on the Go side. Structurally it is the // same as Array — a list carrying a "replace"/"append" operation — but it is a // distinct field type: a collection holds a list of a target entity, whereas an @@ -124,6 +152,25 @@ export class MCollection { return this.operation === "append"; } + static cast( + value: unknown, + ): { ok: boolean; value: MCollection | null } { + if (typeof value === "object" && (value as any)?.__operation) { + const res = MCollection.of((value as any)?.items); + res.operation = (value as any)?.__operation; + + return { + ok: true, + value: res, + }; + } + + return { + value: null, + ok: false, + }; + } + isReplace(): boolean { return this.operation === "replace"; } @@ -167,7 +214,3 @@ export class MCollection { return this.items; } } - -// In javascript, nullability and undefined is already working perfectly fine. -// hence, just extending Collection is enough. -export class MCollectionNullable extends MCollection {} diff --git a/examples/js/test-artifacts/http-envelop-test-output/HttpActionAction.ts b/examples/js/test-artifacts/http-envelop-test-output/HttpActionAction.ts index 30981551..31fbcb5e 100644 --- a/examples/js/test-artifacts/http-envelop-test-output/HttpActionAction.ts +++ b/examples/js/test-artifacts/http-envelop-test-output/HttpActionAction.ts @@ -1,12 +1,4 @@ import { GResponse } from "./sdk/envelopes/index"; -import { - MArray, - MArrayNullable, - MCollection, - MCollectionNullable, - MOne, - MOneNullable, -} from "./sdk/common/operators"; import { buildUrl } from "./sdk/common/buildUrl"; import { fetchx, diff --git a/examples/js/test-artifacts/http-envelop-test-output/sdk/common/operators.ts b/examples/js/test-artifacts/http-envelop-test-output/sdk/common/operators.ts index df3b8e99..6ad63403 100644 --- a/examples/js/test-artifacts/http-envelop-test-output/sdk/common/operators.ts +++ b/examples/js/test-artifacts/http-envelop-test-output/sdk/common/operators.ts @@ -8,6 +8,23 @@ export class MOne { return this.content === null; } + static cast(value: unknown): { ok: boolean; value: MOne | null } { + if (typeof value === "object" && (value as any)?.__operation) { + const res = MOne.of((value as any)?.content); + res.operation = (value as any)?.__operation; + + return { + ok: true, + value: res, + }; + } + + return { + value: null, + ok: false, + }; + } + isSelector(): boolean { return this.selector !== undefined && this.operation !== null; } @@ -45,10 +62,6 @@ export class MOne { } } -// In javascript, nullability and undefined is already working perfectly fine. -// hence, maybe just giving back one is enough. -export class MOneNullable extends MOne {} - // Array describes how an incoming list should be applied to an existing one, // mirroring emigo.Array on the Go side. It lets a PATCH-style payload say // "replace the whole set" versus "append to the existing set". @@ -60,6 +73,25 @@ export class MArray { private operation: "replace" | "append" = "replace"; private items: T[] = []; + static cast( + value: unknown, + ): { ok: boolean; value: MArray | null } { + if (typeof value === "object" && (value as any)?.__operation) { + const res = MArray.of((value as any)?.items); + res.operation = (value as any)?.__operation; + + return { + ok: true, + value: res, + }; + } + + return { + value: null, + ok: false, + }; + } + isAppend(): boolean { return this.operation === "append"; } @@ -108,10 +140,6 @@ export class MArray { } } -// In javascript, nullability and undefined is already working perfectly fine. -// hence, just extending Array is enough. -export class MArrayNullable extends Array {} - // Collection mirrors emigo.Collection on the Go side. Structurally it is the // same as Array — a list carrying a "replace"/"append" operation — but it is a // distinct field type: a collection holds a list of a target entity, whereas an @@ -124,6 +152,25 @@ export class MCollection { return this.operation === "append"; } + static cast( + value: unknown, + ): { ok: boolean; value: MCollection | null } { + if (typeof value === "object" && (value as any)?.__operation) { + const res = MCollection.of((value as any)?.items); + res.operation = (value as any)?.__operation; + + return { + ok: true, + value: res, + }; + } + + return { + value: null, + ok: false, + }; + } + isReplace(): boolean { return this.operation === "replace"; } @@ -167,7 +214,3 @@ export class MCollection { return this.items; } } - -// In javascript, nullability and undefined is already working perfectly fine. -// hence, just extending Collection is enough. -export class MCollectionNullable extends MCollection {} diff --git a/examples/js/test-artifacts/sdk/common/operators.ts b/examples/js/test-artifacts/sdk/common/operators.ts index df3b8e99..31b7af27 100644 --- a/examples/js/test-artifacts/sdk/common/operators.ts +++ b/examples/js/test-artifacts/sdk/common/operators.ts @@ -60,6 +60,25 @@ export class MArray { private operation: "replace" | "append" = "replace"; private items: T[] = []; + static cast( + value: unknown, + ): { ok: boolean; value: MArray | null } { + if (typeof value === "object" && (value as any)?.__operation) { + const res = MArray.of((value as any)?.items); + res.operation = (value as any)?.__operation; + + return { + ok: true, + value: res, + }; + } + + return { + value: null, + ok: false, + }; + } + isAppend(): boolean { return this.operation === "append"; } @@ -128,6 +147,25 @@ export class MCollection { return this.operation === "replace"; } + static cast( + value: unknown, + ): { ok: boolean; value: MCollection | null } { + if (typeof value === "object" && (value as any)?.__operation) { + const res = MCollection.of((value as any)?.items); + res.operation = (value as any)?.__operation; + + return { + ok: true, + value: res, + }; + } + + return { + value: null, + ok: false, + }; + } + len(): number { return this.items.length; } diff --git a/examples/js/test-artifacts/web-socket-test-output/UserStreamAction.ts b/examples/js/test-artifacts/web-socket-test-output/UserStreamAction.ts index 5c0ba161..af64e4b6 100644 --- a/examples/js/test-artifacts/web-socket-test-output/UserStreamAction.ts +++ b/examples/js/test-artifacts/web-socket-test-output/UserStreamAction.ts @@ -1,11 +1,3 @@ -import { - MArray, - MArrayNullable, - MCollection, - MCollectionNullable, - MOne, - MOneNullable, -} from "./sdk/common/operators"; import { WebSocketX } from "./sdk/common/WebSocketX"; import { buildUrl } from "./sdk/common/buildUrl"; import { type PartialDeep } from "./sdk/common/fetchx"; diff --git a/examples/js/test-artifacts/web-socket-test-output/sdk/common/operators.ts b/examples/js/test-artifacts/web-socket-test-output/sdk/common/operators.ts index df3b8e99..6ad63403 100644 --- a/examples/js/test-artifacts/web-socket-test-output/sdk/common/operators.ts +++ b/examples/js/test-artifacts/web-socket-test-output/sdk/common/operators.ts @@ -8,6 +8,23 @@ export class MOne { return this.content === null; } + static cast(value: unknown): { ok: boolean; value: MOne | null } { + if (typeof value === "object" && (value as any)?.__operation) { + const res = MOne.of((value as any)?.content); + res.operation = (value as any)?.__operation; + + return { + ok: true, + value: res, + }; + } + + return { + value: null, + ok: false, + }; + } + isSelector(): boolean { return this.selector !== undefined && this.operation !== null; } @@ -45,10 +62,6 @@ export class MOne { } } -// In javascript, nullability and undefined is already working perfectly fine. -// hence, maybe just giving back one is enough. -export class MOneNullable extends MOne {} - // Array describes how an incoming list should be applied to an existing one, // mirroring emigo.Array on the Go side. It lets a PATCH-style payload say // "replace the whole set" versus "append to the existing set". @@ -60,6 +73,25 @@ export class MArray { private operation: "replace" | "append" = "replace"; private items: T[] = []; + static cast( + value: unknown, + ): { ok: boolean; value: MArray | null } { + if (typeof value === "object" && (value as any)?.__operation) { + const res = MArray.of((value as any)?.items); + res.operation = (value as any)?.__operation; + + return { + ok: true, + value: res, + }; + } + + return { + value: null, + ok: false, + }; + } + isAppend(): boolean { return this.operation === "append"; } @@ -108,10 +140,6 @@ export class MArray { } } -// In javascript, nullability and undefined is already working perfectly fine. -// hence, just extending Array is enough. -export class MArrayNullable extends Array {} - // Collection mirrors emigo.Collection on the Go side. Structurally it is the // same as Array — a list carrying a "replace"/"append" operation — but it is a // distinct field type: a collection holds a list of a target entity, whereas an @@ -124,6 +152,25 @@ export class MCollection { return this.operation === "append"; } + static cast( + value: unknown, + ): { ok: boolean; value: MCollection | null } { + if (typeof value === "object" && (value as any)?.__operation) { + const res = MCollection.of((value as any)?.items); + res.operation = (value as any)?.__operation; + + return { + ok: true, + value: res, + }; + } + + return { + value: null, + ok: false, + }; + } + isReplace(): boolean { return this.operation === "replace"; } @@ -167,7 +214,3 @@ export class MCollection { return this.items; } } - -// In javascript, nullability and undefined is already working perfectly fine. -// hence, just extending Collection is enough. -export class MCollectionNullable extends MCollection {} diff --git a/examples/js/vite.config.ts b/examples/js/vite.config.ts index ac699bcc..20edfcb1 100644 --- a/examples/js/vite.config.ts +++ b/examples/js/vite.config.ts @@ -8,6 +8,21 @@ import WebSocket from "ws"; import { defineConfig as defineVitestConfig } from "vitest/config"; export default defineVitestConfig({ + server: { + watch: { + // The cases write their generated SDK code (User.ts, *.output.ts/js) back + // into the cases/ dir so they can be dynamically imported. Those files are + // part of the module graph, so in `vitest watch` every write would retrigger + // the run, which writes again — an endless refresh loop. Ignore the + // generated artifacts here so the watcher never reacts to them. (These globs + // are merged with Vite's own defaults, e.g. node_modules/.git.) + ignored: [ + "**/cases/User.ts", + "**/cases/*.output.ts", + "**/cases/*.output.js", + ], + }, + }, test: { globals: true, // so you can use "describe", "it", etc. without imports environment: "jsdom", // or 'jsdom' if testing browser code diff --git a/lib/core/common.go b/lib/core/common.go index 8c8e8265..354c8ad6 100644 --- a/lib/core/common.go +++ b/lib/core/common.go @@ -8,6 +8,7 @@ import ( "path/filepath" "reflect" "regexp" + "slices" "strings" "text/template" @@ -411,6 +412,34 @@ func CollectTargets(fields []*EmiField, currentName string) []string { return result } +// Returns true, if any if types mentioned in the argument are ever used +// in the fields tree +func ContainsAnyOfTypes(fields []*EmiField, types []FieldType) bool { + var result bool + + var walk func(f []*EmiField) + walk = func(f []*EmiField) { + for _, field := range f { + if field == nil { + continue + } + + if slices.Contains(types, field.Type) { + result = true + + break + } + + if len(field.Fields) > 0 { + walk(field.Fields) + } + } + } + + walk(fields) + return result +} + func ParseDtoPath(input string) (path string, className string) { if input == "" { return "./", "" diff --git a/lib/js/js-common-fields.go b/lib/js/js-common-fields.go index f5a19766..98d8e9ba 100644 --- a/lib/js/js-common-fields.go +++ b/lib/js/js-common-fields.go @@ -54,7 +54,11 @@ func jsGetSafeFieldValue(field *core.EmiField) string { } switch field.Type { - case "array", "slice", "collection": + case core.FieldTypeArray: + return "MArray.of([])" + case core.FieldTypeCollection: + return "MCollection.of([])" + case core.FieldTypeSlice: return "[]" case "json", "object?", "embed", "computed", "any": return "null" @@ -213,11 +217,17 @@ func jsRenderField( // for non-nullable fields which are late init, we need to make sure instance is being created. isLateInit := !privateFieldToken.IsNullable && privateFieldToken.SafeDefaultValue == "" lateInitStatement := "" - if isLateInit && field.Type == core.FieldTypeObject || field.Type == core.FieldTypeOne { + + if isLateInit && field.Type == core.FieldTypeObject { lateInitStatement = fmt.Sprintf( "if (!(d.%v instanceof %v)) { this.%v = new %v(d.%v || {}) }", field.Name, constructorClass, field.Name, constructorClass, field.Name, ) + } else if isLateInit && field.Type == core.FieldTypeOne { + lateInitStatement = fmt.Sprintf( + "if (!(d.%v instanceof %v)) { this.%v = MOne.of(new %v(d.%v || {})) }", + field.Name, constructorClass, field.Name, constructorClass, field.Name, + ) } staticVariables := []string{} diff --git a/lib/js/js-common-object-class.go b/lib/js/js-common-object-class.go index 76af079a..e2f16b18 100644 --- a/lib/js/js-common-object-class.go +++ b/lib/js/js-common-object-class.go @@ -457,7 +457,7 @@ func tsFieldTypeOnNestedClasses(field *core.EmiField, parentChain string) string target = value } - return target + "[]" + return "MCollection<" + target + ">" case core.FieldTypeOne, core.FieldTypeOneNullable: target := field.Target @@ -467,16 +467,16 @@ func tsFieldTypeOnNestedClasses(field *core.EmiField, parentChain string) string target = value } - return target + return "MOne<" + target + ">" + case core.FieldTypeArray: + return fmt.Sprintf("MArray>", prefix) + case core.FieldTypeArrayNullable: + return fmt.Sprintf("MArray> | null | undefined", prefix) case core.FieldTypeObject: return fmt.Sprintf("InstanceType", prefix) - case core.FieldTypeArray: - return fmt.Sprintf("InstanceType[]", prefix) case core.FieldTypeObjectNullable: return fmt.Sprintf("InstanceType | null | undefined", prefix) - case core.FieldTypeArrayNullable: - return fmt.Sprintf("InstanceType[] | null | undefined", prefix) default: return TsComputedField(field, false, parentChain) } diff --git a/lib/js/js-common-object.go b/lib/js/js-common-object.go index bbb56794..6b91a1a1 100644 --- a/lib/js/js-common-object.go +++ b/lib/js/js-common-object.go @@ -56,17 +56,41 @@ func JsCommonObjectGenerator(fields []*core.EmiField, ctx core.MicroGenContext, return nil, tsClassError } - res.CodeChunkDependensies = append(res.CodeChunkDependensies, core.CodeChunkDependency{ - Objects: []string{ - "MCollection", - "MCollectionNullable", - "MArray", - "MArrayNullable", - "MOne", - "MOneNullable", - }, - Location: getSdkAwareLocation(ctx, INTERNAL_SDK_JS_LOCATION, "operators"), - }) + if core.ContainsAnyOfTypes(fields, []core.FieldType{ + core.FieldTypeCollection, + core.FieldTypeCollectionNullable, + }) { + res.CodeChunkDependensies = append(res.CodeChunkDependensies, core.CodeChunkDependency{ + Objects: []string{ + "MCollection", + }, + Location: getSdkAwareLocation(ctx, INTERNAL_SDK_JS_LOCATION, "operators"), + }) + } + + if core.ContainsAnyOfTypes(fields, []core.FieldType{ + core.FieldTypeArray, + core.FieldTypeArrayNullable, + }) { + res.CodeChunkDependensies = append(res.CodeChunkDependensies, core.CodeChunkDependency{ + Objects: []string{ + "MArray", + }, + Location: getSdkAwareLocation(ctx, INTERNAL_SDK_JS_LOCATION, "operators"), + }) + } + + if core.ContainsAnyOfTypes(fields, []core.FieldType{ + core.FieldTypeOne, + core.FieldTypeOneNullable, + }) { + res.CodeChunkDependensies = append(res.CodeChunkDependensies, core.CodeChunkDependency{ + Objects: []string{ + "MOne", + }, + Location: getSdkAwareLocation(ctx, INTERNAL_SDK_JS_LOCATION, "operators"), + }) + } res.Tokens = append(res.Tokens, tsClass.Tokens...) res.CodeChunkDependensies = append(res.CodeChunkDependensies, tsClass.CodeChunkDependensies...) diff --git a/lib/js/js-sdk/common/operators.js b/lib/js/js-sdk/common/operators.js index 57b1d36f..a1feab5e 100644 --- a/lib/js/js-sdk/common/operators.js +++ b/lib/js/js-sdk/common/operators.js @@ -6,6 +6,20 @@ export class MOne { isNull() { return this.content === null; } + static cast(value) { + if (typeof value === "object" && (value === null || value === void 0 ? void 0 : value.__operation)) { + const res = MOne.of(value === null || value === void 0 ? void 0 : value.content); + res.operation = value === null || value === void 0 ? void 0 : value.__operation; + return { + ok: true, + value: res, + }; + } + return { + value: null, + ok: false, + }; + } isSelector() { return this.selector !== undefined && this.operation !== null; } @@ -35,10 +49,6 @@ export class MOne { return this.content; } } -// In javascript, nullability and undefined is already working perfectly fine. -// hence, maybe just giving back one is enough. -export class MOneNullable extends MOne { -} // Array describes how an incoming list should be applied to an existing one, // mirroring emigo.Array on the Go side. It lets a PATCH-style payload say // "replace the whole set" versus "append to the existing set". @@ -51,6 +61,20 @@ export class MArray { this.operation = "replace"; this.items = []; } + static cast(value) { + if (typeof value === "object" && (value === null || value === void 0 ? void 0 : value.__operation)) { + const res = MArray.of(value === null || value === void 0 ? void 0 : value.items); + res.operation = value === null || value === void 0 ? void 0 : value.__operation; + return { + ok: true, + value: res, + }; + } + return { + value: null, + ok: false, + }; + } isAppend() { return this.operation === "append"; } @@ -89,10 +113,6 @@ export class MArray { return this.items; } } -// In javascript, nullability and undefined is already working perfectly fine. -// hence, just extending Array is enough. -export class MArrayNullable extends Array { -} // Collection mirrors emigo.Collection on the Go side. Structurally it is the // same as Array — a list carrying a "replace"/"append" operation — but it is a // distinct field type: a collection holds a list of a target entity, whereas an @@ -105,6 +125,20 @@ export class MCollection { isAppend() { return this.operation === "append"; } + static cast(value) { + if (typeof value === "object" && (value === null || value === void 0 ? void 0 : value.__operation)) { + const res = MCollection.of(value === null || value === void 0 ? void 0 : value.items); + res.operation = value === null || value === void 0 ? void 0 : value.__operation; + return { + ok: true, + value: res, + }; + } + return { + value: null, + ok: false, + }; + } isReplace() { return this.operation === "replace"; } @@ -140,7 +174,3 @@ export class MCollection { return this.items; } } -// In javascript, nullability and undefined is already working perfectly fine. -// hence, just extending Collection is enough. -export class MCollectionNullable extends MCollection { -} diff --git a/lib/js/js-setter-function.go b/lib/js/js-setter-function.go index f57355d0..2aa235cc 100644 --- a/lib/js/js-setter-function.go +++ b/lib/js/js-setter-function.go @@ -15,6 +15,18 @@ func (x jsFieldVariable) CreateSetterFunction(ctx core.MicroGenContext) string { tsValue := "value: " + x.ComputedType + if core.FieldType(x.Type) == core.FieldTypeOne || core.FieldType(x.Type) == core.FieldTypeOneNullable { + tsValue += " | InstanceType" + } + + if core.FieldType(x.Type) == core.FieldTypeCollection || core.FieldType(x.Type) == core.FieldTypeCollectionNullable { + tsValue += " | InstanceType[]" + } + + if core.FieldType(x.Type) == core.FieldTypeArray || core.FieldType(x.Type) == core.FieldTypeArrayNullable { + tsValue += " | InstanceType[]" + } + if x.IsNullable { tsValue += " | null | undefined" } @@ -28,6 +40,8 @@ func (x jsFieldVariable) CreateSetterFunction(ctx core.MicroGenContext) string { claimsRendered := core.ClaimRender(claims, ctx) var setterTemplate = template.Must(template.New("setter").Parse(` + + {{.ctx.JsDoc}} set {{ .ctx.Name }} (|@arg.value|) { @@ -68,19 +82,122 @@ set {{ .ctx.Name }} (|@arg.value|) { const correctType = value === true || value === false || value === undefined || value === null this.#{{.ctx.Name}} = correctType ? value : Boolean(value); - {{ else if or (eq .ctx.Type "array") (eq .ctx.Type "array?") (eq .ctx.Type "collection?") (eq .ctx.Type "collection")}} - if (!Array.isArray(value) && !(value instanceof MCollection)) { + {{ else if or (eq .ctx.Type "array") (eq .ctx.Type "array?") }} + + {{ if or (eq .ctx.Type "array?") }} + // For nullable array, we allow explicit undefined or null values + if (value === null || value === undefined) { + this.#{{.ctx.Name}} = value; + + return + } + {{ end }} + + // When the passed value is already an array, we check if we need to + // cast the inner items into class instance. + if (Array.isArray(value)) { + if (value.length > 0 && value[0] instanceof {{.ctx.ConstructorClass}}) { + this.#{{.ctx.Name}} = MArray.of(value); + } else { + this.#{{.ctx.Name}} = MArray.of( + value.map((item) => new {{.ctx.ConstructorClass}}(item)), + ); + } + + return; + } + + // If the instance is already an MArray, we assume it's all good. + if (value instanceof MArray) { + this.#{{.ctx.Name}} = value; + + return; + } + + // If the value is not array, and is not a MArray, we need to be consider, + // it might be eligible to be casted into MArray. + {{ if .IsTypeScript }} + const { ok, value: mcastValue } = MArray.cast(value); + if (ok) { + this.#{{.ctx.Name}} = mcastValue as any; + return; + } + {{ else }} + const { ok, value: mcastValue } = MArray.cast(value); + if (ok) { + this.#{{.ctx.Name}} = mcastValue; return; } + {{end }} - if (value.length > 0 && value[0] instanceof {{.ctx.ConstructorClass}}) { + console.warn( + "Cannot assing value to {{.ctx.Name}}, because it needs MArray instance or an Array.", + ); + + {{ else if or (eq .ctx.Type "collection?") (eq .ctx.Type "collection")}} + + {{ if or (eq .ctx.Type "collection?") }} + // For nullable collection, we allow explicit undefined or null values + if (value === null || value === undefined) { + this.#{{.ctx.Name}} = value; + + return + } + {{ end }} + + // When the passed value is already an array, we check if we need to + // cast the inner items into class instance. + if (Array.isArray(value)) { + if (value.length > 0 && value[0] instanceof {{.ctx.ConstructorClass}}) { + this.#{{.ctx.Name}} = MCollection.of(value); + } else { + this.#{{.ctx.Name}} = MCollection.of( + value.map((item) => new {{.ctx.ConstructorClass}}(item)), + ); + } + + return; + } + + // If the instance is already an MCollection, we assume it's all good. + if (value instanceof MCollection) { + this.#{{.ctx.Name}} = value; + + return; + } + + // If the value is not array, and is not a MCollection, we need to be consider, + // it might be eligible to be casted into MCollection. + + {{ if .IsTypeScript }} + const { ok, value: mcastValue } = MCollection.cast(value); + if (ok) { + this.#{{.ctx.Name}} = mcastValue as any; + return; + } + {{ else }} + const { ok, value: mcastValue } = MCollection.cast(value); + if (ok) { + this.#{{.ctx.Name}} = mcastValue; + return; + } + {{ end }} + + console.warn( + "Cannot assing value to {{.ctx.Name}}, because it needs MCollection instance or an Array.", + ); + + {{ else if or (eq .ctx.Type "one") (eq .ctx.Type "one?")}} + // For objects, the sub type needs to always be instance of the sub class. + if (value instanceof MOne) { this.#{{.ctx.Name}} = value + } else if (value instanceof {{.ctx.ConstructorClass}}) { + this.#{{.ctx.Name}} = MOne.of(value) } else { - this.#{{.ctx.Name}} = value.map(item => new {{.ctx.ConstructorClass}}(item)) + this.#{{.ctx.Name}} = MOne.of(new {{.ctx.ConstructorClass}}(value)) } - - {{ else if or (eq .ctx.Type "object") (eq .ctx.Type "object?") (eq .ctx.Type "one") (eq .ctx.Type "one?")}} + {{ else if or (eq .ctx.Type "object") (eq .ctx.Type "object?") }} // For objects, the sub type needs to always be instance of the sub class. if (value instanceof {{.ctx.ConstructorClass}}) { this.#{{.ctx.Name}} = value @@ -101,8 +218,10 @@ set{{ .ctx.Upper }} (|@arg.value|) { setterjsdoc := NewJsDoc(" ") setterjsdoc.Add(fmt.Sprintf("@param {%v}", x.ComputedType)) + isTypeScript := strings.Contains(ctx.Tags, GEN_TYPESCRIPT_COMPATIBILITY) data := map[string]any{ - "ctx": x, + "ctx": x, + "IsTypeScript": isTypeScript, } var buf bytes.Buffer diff --git a/lib/js/ts-sdk/common/operators.ts b/lib/js/ts-sdk/common/operators.ts index df3b8e99..6ad63403 100644 --- a/lib/js/ts-sdk/common/operators.ts +++ b/lib/js/ts-sdk/common/operators.ts @@ -8,6 +8,23 @@ export class MOne { return this.content === null; } + static cast(value: unknown): { ok: boolean; value: MOne | null } { + if (typeof value === "object" && (value as any)?.__operation) { + const res = MOne.of((value as any)?.content); + res.operation = (value as any)?.__operation; + + return { + ok: true, + value: res, + }; + } + + return { + value: null, + ok: false, + }; + } + isSelector(): boolean { return this.selector !== undefined && this.operation !== null; } @@ -45,10 +62,6 @@ export class MOne { } } -// In javascript, nullability and undefined is already working perfectly fine. -// hence, maybe just giving back one is enough. -export class MOneNullable extends MOne {} - // Array describes how an incoming list should be applied to an existing one, // mirroring emigo.Array on the Go side. It lets a PATCH-style payload say // "replace the whole set" versus "append to the existing set". @@ -60,6 +73,25 @@ export class MArray { private operation: "replace" | "append" = "replace"; private items: T[] = []; + static cast( + value: unknown, + ): { ok: boolean; value: MArray | null } { + if (typeof value === "object" && (value as any)?.__operation) { + const res = MArray.of((value as any)?.items); + res.operation = (value as any)?.__operation; + + return { + ok: true, + value: res, + }; + } + + return { + value: null, + ok: false, + }; + } + isAppend(): boolean { return this.operation === "append"; } @@ -108,10 +140,6 @@ export class MArray { } } -// In javascript, nullability and undefined is already working perfectly fine. -// hence, just extending Array is enough. -export class MArrayNullable extends Array {} - // Collection mirrors emigo.Collection on the Go side. Structurally it is the // same as Array — a list carrying a "replace"/"append" operation — but it is a // distinct field type: a collection holds a list of a target entity, whereas an @@ -124,6 +152,25 @@ export class MCollection { return this.operation === "append"; } + static cast( + value: unknown, + ): { ok: boolean; value: MCollection | null } { + if (typeof value === "object" && (value as any)?.__operation) { + const res = MCollection.of((value as any)?.items); + res.operation = (value as any)?.__operation; + + return { + ok: true, + value: res, + }; + } + + return { + value: null, + ok: false, + }; + } + isReplace(): boolean { return this.operation === "replace"; } @@ -167,7 +214,3 @@ export class MCollection { return this.items; } } - -// In javascript, nullability and undefined is already working perfectly fine. -// hence, just extending Collection is enough. -export class MCollectionNullable extends MCollection {} From 6b1e348683d794c9ed59090ce87682c891d55f82 Mon Sep 17 00:00:00 2001 From: Ali Date: Sun, 31 May 2026 14:32:25 +0200 Subject: [PATCH 2/2] Minor fixes in the wrappers --- emigo/Collection.go | 8 +- emigo/CollectionNullable.go | 8 +- ...-js-array-one-collection-wrapper.output.ts | 984 +++++++++--------- 3 files changed, 482 insertions(+), 518 deletions(-) diff --git a/emigo/Collection.go b/emigo/Collection.go index 29823fc2..58954247 100644 --- a/emigo/Collection.go +++ b/emigo/Collection.go @@ -25,7 +25,7 @@ import ( // → Collection{Operation: "replace", Items: [...], isSet: true} // // 3. Tagged object — explicit operation: -// {"__operation": "append", "items": [{"key": "x", "value": "y"}]} +// {"__operation": "append", "__items": [{"key": "x", "value": "y"}]} // → Collection{Operation: "append", Items: [...], isSet: true} // // The vsql renderer treats Collection as opted-out via [Collection.SQLValue] @@ -35,7 +35,7 @@ import ( // directly to drive DELETE/INSERT semantics on child tables. type Collection[T any] struct { Operation string `json:"__operation"` - Items []T `json:"items"` + Items []T `json:"__items"` isSet bool } @@ -105,7 +105,7 @@ func (c Collection[T]) MarshalJSON() ([]byte, error) { } type alias struct { Operation string `json:"__operation"` - Items []T `json:"items"` + Items []T `json:"__items"` } // When operation is replace, we simply return the content @@ -141,7 +141,7 @@ func (c *Collection[T]) UnmarshalJSON(data []byte) error { } type alias struct { Operation string `json:"__operation"` - Items []T `json:"items"` + Items []T `json:"__items"` } var a alias if err := json.Unmarshal(data, &a); err != nil { diff --git a/emigo/CollectionNullable.go b/emigo/CollectionNullable.go index 6a6479a3..ff1bdc8b 100644 --- a/emigo/CollectionNullable.go +++ b/emigo/CollectionNullable.go @@ -25,7 +25,7 @@ import ( // → CollectionNullable{Operation: "replace", Items: [...], isSet: true} // // 3. Tagged object — explicit operation: -// {"__operation": "append", "items": [{"key": "x", "value": "y"}]} +// {"__operation": "append", "__items": [{"key": "x", "value": "y"}]} // → CollectionNullable{Operation: "append", Items: [...], isSet: true} // // The vsql renderer treats CollectionNullable as opted-out via [CollectionNullable.SQLValue] @@ -35,7 +35,7 @@ import ( // directly to drive DELETE/INSERT semantics on child tables. type CollectionNullable[T any] struct { Operation string `json:"__operation"` - Items []T `json:"items"` + Items []T `json:"__items"` isSet bool } @@ -105,7 +105,7 @@ func (c CollectionNullable[T]) MarshalJSON() ([]byte, error) { } type alias struct { Operation string `json:"__operation"` - Items []T `json:"items"` + Items []T `json:"__items"` } // When operation is replace, we simply return the content @@ -141,7 +141,7 @@ func (c *CollectionNullable[T]) UnmarshalJSON(data []byte) error { } type alias struct { Operation string `json:"__operation"` - Items []T `json:"items"` + Items []T `json:"__items"` } var a alias if err := json.Unmarshal(data, &a); err != nil { diff --git a/examples/js/cases/emi-js-array-one-collection-wrapper.output.ts b/examples/js/cases/emi-js-array-one-collection-wrapper.output.ts index 831b450c..9851851b 100644 --- a/examples/js/cases/emi-js-array-one-collection-wrapper.output.ts +++ b/examples/js/cases/emi-js-array-one-collection-wrapper.output.ts @@ -1,517 +1,481 @@ -import { MArray, MCollection, MOne } from "./sdk/common/operators"; -import { User } from "./User"; -import { type PartialDeep } from "./sdk/common/fetchx"; -import { withPrefix } from "./sdk/common/withPrefix"; -/** - * The base class definition for anonymouse - **/ +import { MArray, MCollection, MOne } from './sdk/common/operators'; +import { User } from './User'; +import { type PartialDeep } from './sdk/common/fetchx'; +import { withPrefix } from './sdk/common/withPrefix'; +/** + * The base class definition for anonymouse + **/ export class Anonymouse { - /** - * array field, non-nullable - * @type {Anonymouse.ArrayField} - **/ - #arrayField: MArray> = MArray.of( - [], - ); - /** - * array field, non-nullable - * @returns {Anonymouse.ArrayField} - **/ - get arrayField() { - return this.#arrayField; - } - /** - * array field, non-nullable - * @type {Anonymouse.ArrayField} - **/ - set arrayField( - value: - | MArray> - | InstanceType[], - ) { - // When the passed value is already an array, we check if we need to - // cast the inner items into class instance. - if (Array.isArray(value)) { - if (value.length > 0 && value[0] instanceof Anonymouse.ArrayField) { - this.#arrayField = MArray.of(value); - } else { - this.#arrayField = MArray.of( - value.map((item) => new Anonymouse.ArrayField(item)), - ); - } - return; - } - // If the instance is already an MArray, we assume it's all good. - if (value instanceof MArray) { - this.#arrayField = value; - return; - } - // If the value is not array, and is not a MArray, we need to be consider, - // it might be eligible to be casted into MArray. - const { ok, value: mcastValue } = MArray.cast(value); - if (ok) { - this.#arrayField = mcastValue as any; - return; - } - console.warn( - "Cannot assing value to arrayField, because it needs MArray instance or an Array.", - ); - } - setArrayField( - value: - | MArray> - | InstanceType[], - ) { - this.arrayField = value; - return this; - } - /** - * arrayNullable field, non-nullable - * @type {any} - **/ - #arrayNullableField!: any; - /** - * arrayNullable field, non-nullable - * @returns {any} - **/ - get arrayNullableField() { - return this.#arrayNullableField; - } - /** - * arrayNullable field, non-nullable - * @type {any} - **/ - set arrayNullableField(value: any) { - this.#arrayNullableField = value; - } - setArrayNullableField(value: any) { - this.arrayNullableField = value; - return this; - } - /** - * collection field, non-nullable - * @type {User[]} - **/ - #collectionField: MCollection = MCollection.of([]); - /** - * collection field, non-nullable - * @returns {User[]} - **/ - get collectionField() { - return this.#collectionField; - } - /** - * collection field, non-nullable - * @type {User[]} - **/ - set collectionField(value: MCollection | InstanceType[]) { - // When the passed value is already an array, we check if we need to - // cast the inner items into class instance. - if (Array.isArray(value)) { - if (value.length > 0 && value[0] instanceof User) { - this.#collectionField = MCollection.of(value); - } else { - this.#collectionField = MCollection.of( - value.map((item) => new User(item)), - ); - } - return; - } - // If the instance is already an MCollection, we assume it's all good. - if (value instanceof MCollection) { - this.#collectionField = value; - return; - } - // If the value is not array, and is not a MCollection, we need to be consider, - // it might be eligible to be casted into MCollection. - const { ok, value: mcastValue } = MCollection.cast(value); - if (ok) { - this.#collectionField = mcastValue as any; - return; - } - console.warn( - "Cannot assing value to collectionField, because it needs MCollection instance or an Array.", - ); - } - setCollectionField(value: MCollection | InstanceType[]) { - this.collectionField = value; - return this; - } - /** - * collectionNullable field, non-nullable - * @type {User[]} - **/ - #collectionNullableField?: MCollection | null = undefined; - /** - * collectionNullable field, non-nullable - * @returns {User[]} - **/ - get collectionNullableField() { - return this.#collectionNullableField; - } - /** - * collectionNullable field, non-nullable - * @type {User[]} - **/ - set collectionNullableField( - value: MCollection | InstanceType[] | null | undefined, - ) { - // For nullable collection, we allow explicit undefined or null values - if (value === null || value === undefined) { - this.#collectionNullableField = value; - return; - } - // When the passed value is already an array, we check if we need to - // cast the inner items into class instance. - if (Array.isArray(value)) { - if (value.length > 0 && value[0] instanceof User) { - this.#collectionNullableField = MCollection.of(value); - } else { - this.#collectionNullableField = MCollection.of( - value.map((item) => new User(item)), - ); - } - return; - } - // If the instance is already an MCollection, we assume it's all good. - if (value instanceof MCollection) { - this.#collectionNullableField = value; - return; - } - // If the value is not array, and is not a MCollection, we need to be consider, - // it might be eligible to be casted into MCollection. - const { ok, value: mcastValue } = MCollection.cast(value); - if (ok) { - this.#collectionNullableField = mcastValue as any; - return; - } - console.warn( - "Cannot assing value to collectionNullableField, because it needs MCollection instance or an Array.", - ); - } - setCollectionNullableField( - value: MCollection | InstanceType[] | null | undefined, - ) { - this.collectionNullableField = value; - return this; - } - /** - * one field, non-nullable - * @type {User} - **/ - #oneField!: MOne; - /** - * one field, non-nullable - * @returns {User} - **/ - get oneField() { - return this.#oneField; - } - /** - * one field, non-nullable - * @type {User} - **/ - set oneField(value: MOne | InstanceType) { - // For objects, the sub type needs to always be instance of the sub class. - if (value instanceof MOne) { - this.#oneField = value; - } else if (value instanceof User) { - this.#oneField = MOne.of(value); - } else { - this.#oneField = MOne.of(new User(value)); - } - } - setOneField(value: MOne | InstanceType) { - this.oneField = value; - return this; - } - /** - * oneNullable field, non-nullable - * @type {User} - **/ - #oneNullableField?: MOne | null = undefined; - /** - * oneNullable field, non-nullable - * @returns {User} - **/ - get oneNullableField() { - return this.#oneNullableField; - } - /** - * oneNullable field, non-nullable - * @type {User} - **/ - set oneNullableField( - value: MOne | InstanceType | null | undefined, - ) { - // For objects, the sub type needs to always be instance of the sub class. - if (value instanceof MOne) { - this.#oneNullableField = value; - } else if (value instanceof User) { - this.#oneNullableField = MOne.of(value); - } else { - this.#oneNullableField = MOne.of(new User(value)); - } - } - setOneNullableField( - value: MOne | InstanceType | null | undefined, - ) { - this.oneNullableField = value; - return this; - } - /** - * The base class definition for arrayField - **/ - static ArrayField = class ArrayField { - constructor(data: unknown = undefined) { - if (data === null || data === undefined) { - return; - } - if (typeof data === "string") { - this.applyFromObject(JSON.parse(data)); - } else if (this.#isJsonAppliable(data)) { - this.applyFromObject(data); - } else { - throw new Error( - "Instance cannot be created on an unknown value, check the content being passed. got: " + - typeof data, - ); - } - } - #isJsonAppliable(obj: unknown) { - const g = globalThis as unknown as { Buffer: any; Blob: any }; - const isBuffer = - typeof g.Buffer !== "undefined" && - typeof g.Buffer.isBuffer === "function" && - g.Buffer.isBuffer(obj); - const isBlob = typeof g.Blob !== "undefined" && obj instanceof g.Blob; - return ( - obj && - typeof obj === "object" && - !Array.isArray(obj) && - !isBuffer && - !(obj instanceof ArrayBuffer) && - !isBlob - ); - } - /** - * casts the fields of a javascript object into the class properties one by one - **/ - applyFromObject(data = {}) { - const d = data as Partial; - } - /** - * Special toJSON override, since the field are private, - * Json stringify won't see them unless we mention it explicitly. - **/ - toJSON() { - return {}; - } - toString() { - return JSON.stringify(this); - } - static get Fields() { - return {}; - } - /** - * Creates an instance of Anonymouse.ArrayField, and possibleDtoObject - * needs to satisfy the type requirement fully, otherwise typescript compile would - * be complaining. - **/ - static from(possibleDtoObject: AnonymouseType.ArrayFieldType) { - return new Anonymouse.ArrayField(possibleDtoObject); - } - /** - * Creates an instance of Anonymouse.ArrayField, and partialDtoObject - * needs to satisfy the type, but partially, and rest of the content would - * be constructed according to data types and nullability. - **/ - static with(partialDtoObject: PartialDeep) { - return new Anonymouse.ArrayField(partialDtoObject); - } - copyWith( - partial: PartialDeep, - ): InstanceType { - return new Anonymouse.ArrayField({ ...this.toJSON(), ...partial }); - } - clone(): InstanceType { - return new Anonymouse.ArrayField(this.toJSON()); - } - }; - constructor(data: unknown = undefined) { - if (data === null || data === undefined) { - this.#lateInitFields(); - return; - } - if (typeof data === "string") { - this.applyFromObject(JSON.parse(data)); - } else if (this.#isJsonAppliable(data)) { - this.applyFromObject(data); - } else { - throw new Error( - "Instance cannot be created on an unknown value, check the content being passed. got: " + - typeof data, - ); - } - } - #isJsonAppliable(obj: unknown) { - const g = globalThis as unknown as { Buffer: any; Blob: any }; - const isBuffer = - typeof g.Buffer !== "undefined" && - typeof g.Buffer.isBuffer === "function" && - g.Buffer.isBuffer(obj); - const isBlob = typeof g.Blob !== "undefined" && obj instanceof g.Blob; - return ( - obj && - typeof obj === "object" && - !Array.isArray(obj) && - !isBuffer && - !(obj instanceof ArrayBuffer) && - !isBlob - ); - } - /** - * casts the fields of a javascript object into the class properties one by one - **/ - applyFromObject(data = {}) { - const d = data as Partial; - if (d.arrayField !== undefined) { - this.arrayField = d.arrayField; - } - if (d.arrayNullableField !== undefined) { - this.arrayNullableField = d.arrayNullableField; - } - if (d.collectionField !== undefined) { - this.collectionField = d.collectionField; - } - if (d.collectionNullableField !== undefined) { - this.collectionNullableField = d.collectionNullableField; - } - if (d.oneField !== undefined) { - this.oneField = d.oneField; - } - if (d.oneNullableField !== undefined) { - this.oneNullableField = d.oneNullableField; - } - this.#lateInitFields(data); - } - /** - * These are the class instances, which need to be initialised, regardless of the constructor incoming data - **/ - #lateInitFields(data = {}) { - const d = data as Partial; - if (!(d.oneField instanceof User)) { - this.oneField = MOne.of(new User(d.oneField || {})); - } - } - /** - * Special toJSON override, since the field are private, - * Json stringify won't see them unless we mention it explicitly. - **/ - toJSON() { - return { - arrayField: this.#arrayField, - arrayNullableField: this.#arrayNullableField, - collectionField: this.#collectionField, - collectionNullableField: this.#collectionNullableField, - oneField: this.#oneField, - oneNullableField: this.#oneNullableField, - }; - } - toString() { - return JSON.stringify(this); - } - static get Fields() { - return { - arrayField$: "arrayField", - get arrayField() { - return withPrefix("arrayField[:i]", Anonymouse.ArrayField.Fields); - }, - arrayNullableField: "arrayNullableField", - collectionField$: "collectionField", - get collectionField() { - return withPrefix("collectionField[:i]", User.Fields); - }, - collectionNullableField$: "collectionNullableField", - get collectionNullableField() { - return withPrefix("collectionNullableField", User.Fields); - }, - oneField$: "oneField", - get oneField() { - return withPrefix("oneField", User.Fields); - }, - oneNullableField: "oneNullableField", - }; - } - /** - * Creates an instance of Anonymouse, and possibleDtoObject - * needs to satisfy the type requirement fully, otherwise typescript compile would - * be complaining. - **/ - static from(possibleDtoObject: AnonymouseType) { - return new Anonymouse(possibleDtoObject); - } - /** - * Creates an instance of Anonymouse, and partialDtoObject - * needs to satisfy the type, but partially, and rest of the content would - * be constructed according to data types and nullability. - **/ - static with(partialDtoObject: PartialDeep) { - return new Anonymouse(partialDtoObject); - } - copyWith( - partial: PartialDeep, - ): InstanceType { - return new Anonymouse({ ...this.toJSON(), ...partial }); - } - clone(): InstanceType { - return new Anonymouse(this.toJSON()); - } + /** + * array field, non-nullable + * @type {Anonymouse.ArrayField} + **/ + #arrayField : MArray> = MArray.of([]) + /** + * array field, non-nullable + * @returns {Anonymouse.ArrayField} + **/ +get arrayField () { return this.#arrayField } +/** + * array field, non-nullable + * @type {Anonymouse.ArrayField} + **/ +set arrayField (value: MArray> | InstanceType[]) { + // When the passed value is already an array, we check if we need to + // cast the inner items into class instance. + if (Array.isArray(value)) { + if (value.length > 0 && value[0] instanceof Anonymouse.ArrayField) { + this.#arrayField = MArray.of(value); + } else { + this.#arrayField = MArray.of( + value.map((item) => new Anonymouse.ArrayField(item)), + ); + } + return; + } + // If the instance is already an MArray, we assume it's all good. + if (value instanceof MArray) { + this.#arrayField = value; + return; + } + // If the value is not array, and is not a MArray, we need to be consider, + // it might be eligible to be casted into MArray. + const { ok, value: mcastValue } = MArray.cast(value); + if (ok) { + this.#arrayField = mcastValue as any; + return; + } + console.warn( + "Cannot assing value to arrayField, because it needs MArray instance or an Array.", + ); +} +setArrayField (value: MArray> | InstanceType[]) { + this.arrayField = value + return this +} + /** + * arrayNullable field, non-nullable + * @type {any} + **/ + #arrayNullableField ! : any + /** + * arrayNullable field, non-nullable + * @returns {any} + **/ +get arrayNullableField () { return this.#arrayNullableField } +/** + * arrayNullable field, non-nullable + * @type {any} + **/ +set arrayNullableField (value: any) { + this.#arrayNullableField = value; +} +setArrayNullableField (value: any) { + this.arrayNullableField = value + return this +} + /** + * collection field, non-nullable + * @type {User[]} + **/ + #collectionField : MCollection = MCollection.of([]) + /** + * collection field, non-nullable + * @returns {User[]} + **/ +get collectionField () { return this.#collectionField } +/** + * collection field, non-nullable + * @type {User[]} + **/ +set collectionField (value: MCollection | InstanceType[]) { + // When the passed value is already an array, we check if we need to + // cast the inner items into class instance. + if (Array.isArray(value)) { + if (value.length > 0 && value[0] instanceof User) { + this.#collectionField = MCollection.of(value); + } else { + this.#collectionField = MCollection.of( + value.map((item) => new User(item)), + ); + } + return; + } + // If the instance is already an MCollection, we assume it's all good. + if (value instanceof MCollection) { + this.#collectionField = value; + return; + } + // If the value is not array, and is not a MCollection, we need to be consider, + // it might be eligible to be casted into MCollection. + const { ok, value: mcastValue } = MCollection.cast(value); + if (ok) { + this.#collectionField = mcastValue as any; + return; + } + console.warn( + "Cannot assing value to collectionField, because it needs MCollection instance or an Array.", + ); +} +setCollectionField (value: MCollection | InstanceType[]) { + this.collectionField = value + return this +} + /** + * collectionNullable field, non-nullable + * @type {User[]} + **/ + #collectionNullableField ? : MCollection | null = undefined + /** + * collectionNullable field, non-nullable + * @returns {User[]} + **/ +get collectionNullableField () { return this.#collectionNullableField } +/** + * collectionNullable field, non-nullable + * @type {User[]} + **/ +set collectionNullableField (value: MCollection | InstanceType[] | null | undefined) { + // For nullable collection, we allow explicit undefined or null values + if (value === null || value === undefined) { + this.#collectionNullableField = value; + return + } + // When the passed value is already an array, we check if we need to + // cast the inner items into class instance. + if (Array.isArray(value)) { + if (value.length > 0 && value[0] instanceof User) { + this.#collectionNullableField = MCollection.of(value); + } else { + this.#collectionNullableField = MCollection.of( + value.map((item) => new User(item)), + ); + } + return; + } + // If the instance is already an MCollection, we assume it's all good. + if (value instanceof MCollection) { + this.#collectionNullableField = value; + return; + } + // If the value is not array, and is not a MCollection, we need to be consider, + // it might be eligible to be casted into MCollection. + const { ok, value: mcastValue } = MCollection.cast(value); + if (ok) { + this.#collectionNullableField = mcastValue as any; + return; + } + console.warn( + "Cannot assing value to collectionNullableField, because it needs MCollection instance or an Array.", + ); +} +setCollectionNullableField (value: MCollection | InstanceType[] | null | undefined) { + this.collectionNullableField = value + return this +} + /** + * one field, non-nullable + * @type {User} + **/ + #oneField ! : MOne + /** + * one field, non-nullable + * @returns {User} + **/ +get oneField () { return this.#oneField } +/** + * one field, non-nullable + * @type {User} + **/ +set oneField (value: MOne | InstanceType) { + // For objects, the sub type needs to always be instance of the sub class. + if (value instanceof MOne) { + this.#oneField = value + } else if (value instanceof User) { + this.#oneField = MOne.of(value) + } else { + this.#oneField = MOne.of(new User(value)) + } +} +setOneField (value: MOne | InstanceType) { + this.oneField = value + return this +} + /** + * oneNullable field, non-nullable + * @type {User} + **/ + #oneNullableField ? : MOne | null = undefined + /** + * oneNullable field, non-nullable + * @returns {User} + **/ +get oneNullableField () { return this.#oneNullableField } +/** + * oneNullable field, non-nullable + * @type {User} + **/ +set oneNullableField (value: MOne | InstanceType | null | undefined) { + // For objects, the sub type needs to always be instance of the sub class. + if (value instanceof MOne) { + this.#oneNullableField = value + } else if (value instanceof User) { + this.#oneNullableField = MOne.of(value) + } else { + this.#oneNullableField = MOne.of(new User(value)) + } +} +setOneNullableField (value: MOne | InstanceType | null | undefined) { + this.oneNullableField = value + return this +} +/** + * The base class definition for arrayField + **/ +static ArrayField = class ArrayField { + constructor(data: unknown = undefined) { + if (data === null || data === undefined) { + return; + } + if (typeof data === "string") { + this.applyFromObject(JSON.parse(data)); + } else if (this.#isJsonAppliable(data)) { + this.applyFromObject(data); + } else { + throw new Error("Instance cannot be created on an unknown value, check the content being passed. got: " + typeof data); + } + } + #isJsonAppliable(obj: unknown) { + const g = globalThis as unknown as { Buffer: any; Blob: any }; + const isBuffer = + typeof g.Buffer !== "undefined" && + typeof g.Buffer.isBuffer === "function" && + g.Buffer.isBuffer(obj); + const isBlob = + typeof g.Blob !== "undefined" && obj instanceof g.Blob; + return ( + obj && + typeof obj === "object" && + !Array.isArray(obj) && + !isBuffer && + !(obj instanceof ArrayBuffer) && + !isBlob + ); + } + /** + * casts the fields of a javascript object into the class properties one by one + **/ + applyFromObject(data = {}) { + const d = data as Partial; + } + /** + * Special toJSON override, since the field are private, + * Json stringify won't see them unless we mention it explicitly. + **/ + toJSON() { + return { + }; + } + toString() { + return JSON.stringify(this); + } + static get Fields() { + return { + } + } + /** + * Creates an instance of Anonymouse.ArrayField, and possibleDtoObject + * needs to satisfy the type requirement fully, otherwise typescript compile would + * be complaining. + **/ + static from(possibleDtoObject: AnonymouseType.ArrayFieldType) { + return new Anonymouse.ArrayField(possibleDtoObject); + } + /** + * Creates an instance of Anonymouse.ArrayField, and partialDtoObject + * needs to satisfy the type, but partially, and rest of the content would + * be constructed according to data types and nullability. + **/ + static with(partialDtoObject: PartialDeep) { + return new Anonymouse.ArrayField(partialDtoObject); + } + copyWith(partial: PartialDeep): InstanceType { + return new Anonymouse.ArrayField ({ ...this.toJSON(), ...partial }); + } + clone(): InstanceType { + return new Anonymouse.ArrayField(this.toJSON()); + } +} + constructor(data: unknown = undefined) { + if (data === null || data === undefined) { + this.#lateInitFields(); + return; + } + if (typeof data === "string") { + this.applyFromObject(JSON.parse(data)); + } else if (this.#isJsonAppliable(data)) { + this.applyFromObject(data); + } else { + throw new Error("Instance cannot be created on an unknown value, check the content being passed. got: " + typeof data); + } + } + #isJsonAppliable(obj: unknown) { + const g = globalThis as unknown as { Buffer: any; Blob: any }; + const isBuffer = + typeof g.Buffer !== "undefined" && + typeof g.Buffer.isBuffer === "function" && + g.Buffer.isBuffer(obj); + const isBlob = + typeof g.Blob !== "undefined" && obj instanceof g.Blob; + return ( + obj && + typeof obj === "object" && + !Array.isArray(obj) && + !isBuffer && + !(obj instanceof ArrayBuffer) && + !isBlob + ); + } + /** + * casts the fields of a javascript object into the class properties one by one + **/ + applyFromObject(data = {}) { + const d = data as Partial; + if (d.arrayField !== undefined) { this.arrayField = d.arrayField } + if (d.arrayNullableField !== undefined) { this.arrayNullableField = d.arrayNullableField } + if (d.collectionField !== undefined) { this.collectionField = d.collectionField } + if (d.collectionNullableField !== undefined) { this.collectionNullableField = d.collectionNullableField } + if (d.oneField !== undefined) { this.oneField = d.oneField } + if (d.oneNullableField !== undefined) { this.oneNullableField = d.oneNullableField } + this.#lateInitFields(data) + } + /** + * These are the class instances, which need to be initialised, regardless of the constructor incoming data + **/ + #lateInitFields(data = {}) { + const d = data as Partial; + if (!(d.oneField instanceof User)) { this.oneField = MOne.of(new User(d.oneField || {})) } + } + /** + * Special toJSON override, since the field are private, + * Json stringify won't see them unless we mention it explicitly. + **/ + toJSON() { + return { + arrayField: this.#arrayField, + arrayNullableField: this.#arrayNullableField, + collectionField: this.#collectionField, + collectionNullableField: this.#collectionNullableField, + oneField: this.#oneField, + oneNullableField: this.#oneNullableField, + }; + } + toString() { + return JSON.stringify(this); + } + static get Fields() { + return { + arrayField$: 'arrayField', +get arrayField() { + return withPrefix( + "arrayField[:i]", + Anonymouse.ArrayField.Fields + ); + }, + arrayNullableField: 'arrayNullableField', + collectionField$: 'collectionField', +get collectionField() { + return withPrefix( + "collectionField[:i]", + User.Fields + ); + }, + collectionNullableField$: 'collectionNullableField', +get collectionNullableField() { + return withPrefix( + "collectionNullableField", + User.Fields + ); + }, + oneField$: 'oneField', +get oneField() { + return withPrefix( + "oneField", + User.Fields + ); + }, + oneNullableField: 'oneNullableField', + } + } + /** + * Creates an instance of Anonymouse, and possibleDtoObject + * needs to satisfy the type requirement fully, otherwise typescript compile would + * be complaining. + **/ + static from(possibleDtoObject: AnonymouseType) { + return new Anonymouse(possibleDtoObject); + } + /** + * Creates an instance of Anonymouse, and partialDtoObject + * needs to satisfy the type, but partially, and rest of the content would + * be constructed according to data types and nullability. + **/ + static with(partialDtoObject: PartialDeep) { + return new Anonymouse(partialDtoObject); + } + copyWith(partial: PartialDeep): InstanceType { + return new Anonymouse ({ ...this.toJSON(), ...partial }); + } + clone(): InstanceType { + return new Anonymouse(this.toJSON()); + } } export abstract class AnonymouseFactory { - abstract create(data: unknown): Anonymouse; + abstract create(data: unknown): Anonymouse; } -/** - * The base type definition for anonymouse - **/ -export type AnonymouseType = { - /** - * array field, non-nullable - * @type {any[]} - **/ - arrayField: any[]; - /** - * arrayNullable field, non-nullable - * @type {any} - **/ - arrayNullableField: any; - /** - * collection field, non-nullable - * @type {User[]} - **/ - collectionField: User[]; - /** - * collectionNullable field, non-nullable - * @type {User[]} - **/ - collectionNullableField?: User[]; - /** - * one field, non-nullable - * @type {User} - **/ - oneField: User; - /** - * oneNullable field, non-nullable - * @type {User} - **/ - oneNullableField?: User; -}; + /** + * The base type definition for anonymouse + **/ + export type AnonymouseType = { + /** + * array field, non-nullable + * @type {any[]} + **/ + arrayField : any[]; + /** + * arrayNullable field, non-nullable + * @type {any} + **/ + arrayNullableField : any; + /** + * collection field, non-nullable + * @type {User[]} + **/ + collectionField : User[]; + /** + * collectionNullable field, non-nullable + * @type {User[]} + **/ + collectionNullableField ?: User[]; + /** + * one field, non-nullable + * @type {User} + **/ + oneField : User; + /** + * oneNullable field, non-nullable + * @type {User} + **/ + oneNullableField ?: User; + } // eslint-disable-next-line @typescript-eslint/no-namespace export namespace AnonymouseType { - /** - * The base type definition for arrayFieldType - **/ - export type ArrayFieldType = {}; - // eslint-disable-next-line @typescript-eslint/no-namespace - export namespace ArrayFieldType {} + /** + * The base type definition for arrayFieldType + **/ + export type ArrayFieldType = { + } +// eslint-disable-next-line @typescript-eslint/no-namespace +export namespace ArrayFieldType { } +} \ No newline at end of file