From 82ddbbe034afde1d29a6dfa1e3bf23a18c04d38f Mon Sep 17 00:00:00 2001 From: Eyas Sharaiha Date: Sat, 31 Jul 2021 16:43:14 -0400 Subject: [PATCH] Add Type-safe strings for Number, Integer, Float Schema.org Numbers, Integers, and Floats support being assigned to strings with the appropriate fromat (e.g. "4394" for Integer). With TypeScript 4.1.0 and beyond, this can be expressed with the `` `${number}` `` type. This pushes the minimum required version for schema-dts from 3.4.0 to 4.1.0, and is thus a breaking change. Note there's a future situation where we would RESTRICT the `Integer` type further; so that only valid integral numbers are assignable to it. This will be another breaking change for another time, when TypeScript can finally represent this. Fixes #130. --- dist/schema/package.json | 4 +- package-lock.json | 6 +- package.json | 4 +- src/transform/toClass.ts | 20 ++++-- src/ts/class.ts | 49 ++++++++----- test/baselines/comments_test.ts | 2 +- test/baselines/data_type_union_test.ts | 2 +- test/baselines/deprecated_objects_test.ts | 2 +- test/baselines/inheritance_multiple_test.ts | 2 +- test/baselines/inheritance_one_test.ts | 2 +- test/baselines/nodeprecated_objects_test.ts | 2 +- test/baselines/sorted_proptypes_test.ts | 2 +- test/ts/class_test.ts | 78 +++++++++++++-------- 13 files changed, 107 insertions(+), 68 deletions(-) diff --git a/dist/schema/package.json b/dist/schema/package.json index 817a976..5dbb078 100644 --- a/dist/schema/package.json +++ b/dist/schema/package.json @@ -1,6 +1,6 @@ { "name": "schema-dts", - "version": "0.9.0", + "version": "0.10.0", "displayName": "schema-dts: Strongly-typed Schema.org vocabulary declarations", "description": "A TypeScript package with latest Schema.org Schema Typings", "authors": [ @@ -17,7 +17,7 @@ "devDependencies": {}, "dependencies": {}, "peerDependencies": { - "typescript": ">=3.4.0" + "typescript": ">=4.1.0" }, "keywords": [ "typescript", diff --git a/package-lock.json b/package-lock.json index fc48912..2fc8359 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "schema-dts-gen", - "version": "0.9.0", + "version": "0.10.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "schema-dts-gen", - "version": "0.9.0", + "version": "0.10.0", "license": "Apache-2.0", "dependencies": { "argparse": "^2.0.1", @@ -47,7 +47,7 @@ "unified": "^9.2.1" }, "peerDependencies": { - "typescript": ">=3.4.0" + "typescript": ">=4.1.0" } }, "node_modules/@babel/code-frame": { diff --git a/package.json b/package.json index 3c5d550..d257e0f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "schema-dts-gen", - "version": "0.9.0", + "version": "0.10.0", "displayName": "schema-dts Generator", "description": "Generate TypeScript Definitions for Schema.org Schema", "authors": [ @@ -54,7 +54,7 @@ "rxjs": "^7.2.0" }, "peerDependencies": { - "typescript": ">=3.4.0" + "typescript": ">=4.1.0" }, "nyc": { "extension": [ diff --git a/src/transform/toClass.ts b/src/transform/toClass.ts index 5514e52..cc8248a 100644 --- a/src/transform/toClass.ts +++ b/src/transform/toClass.ts @@ -39,12 +39,18 @@ function toClass(cls: Class, topic: Topic, map: ClassMap): Class { } const wellKnownTypes = [ - new AliasBuiltin('http://schema.org/Text', 'string'), - new AliasBuiltin('http://schema.org/Number', 'number'), - new AliasBuiltin('http://schema.org/Time', 'string'), - new AliasBuiltin('http://schema.org/Date', 'string'), - new AliasBuiltin('http://schema.org/DateTime', 'string'), - new AliasBuiltin('http://schema.org/Boolean', 'boolean'), + new AliasBuiltin('http://schema.org/Text', AliasBuiltin.Alias('string')), + // IMPORTANT: In the future, if possible, we should have: `${number}` in Float only, + // an integer string literal in Integer only, and Number becomes simply Float|Integer. + new AliasBuiltin( + 'http://schema.org/Number', + AliasBuiltin.Alias('number'), + AliasBuiltin.NumberStringLiteral() + ), + new AliasBuiltin('http://schema.org/Time', AliasBuiltin.Alias('string')), + new AliasBuiltin('http://schema.org/Date', AliasBuiltin.Alias('string')), + new AliasBuiltin('http://schema.org/DateTime', AliasBuiltin.Alias('string')), + new AliasBuiltin('http://schema.org/Boolean', AliasBuiltin.Alias('boolean')), ]; // Should we allow 'string' to be a valid type for all values of this type? @@ -85,7 +91,7 @@ function ForwardDeclareClasses(topics: readonly TypedTopic[]): ClassMap { const allowString = wellKnownStrings.some(wks => wks.equivTo(topic.Subject) ); - if (allowString) cls.addTypedef('string'); + if (allowString) cls.addTypedef(AliasBuiltin.Alias('string')); classes.set(topic.Subject.toString(), cls); } diff --git a/src/ts/class.ts b/src/ts/class.ts index f403b3c..adc9f0f 100644 --- a/src/ts/class.ts +++ b/src/ts/class.ts @@ -61,7 +61,7 @@ export type ClassMap = Map; */ export class Class { private _comment?: string; - private _typedef?: string; + private _typedefs: TypeNode[] = []; private readonly children: Class[] = []; private readonly _parents: Class[] = []; private readonly _props: Set = new Set(); @@ -97,11 +97,15 @@ export class Class { return appendLine(this._comment, deprecated); } - protected get typedefs(): string[] { + protected get typedefs(): TypeNode[] { + const f: TypeNode = undefined!; + const parents = this.allParents().flatMap(p => p.typedefs); return Array.from( - new Set(this._typedef ? [this._typedef, ...parents] : parents) - ).sort(); + new Map([...this._typedefs, ...parents].map(t => [JSON.stringify(t), t])) + ) + .sort(([key1, value1], [key2, value2]) => key1.localeCompare(key2)) + .map(([_, value]) => value); } private properties() { @@ -198,13 +202,8 @@ export class Class { return false; } - addTypedef(typedef: string) { - if (this._typedef) { - throw new Error( - `Class ${this.subject.href} already has typedef ${this._typedef} but we're also adding ${typedef}` - ); - } - this._typedef = typedef; + addTypedef(typedef: TypeNode) { + this._typedefs.push(typedef); } addProp(p: Property) { @@ -299,7 +298,7 @@ export class Class { protected nonEnumType(skipDeprecated: boolean): TypeNode[] { this.children.sort((a, b) => CompareKeys(a.subject, b.subject)); - const children = this.children + const children: TypeNode[] = this.children .filter(child => !(child.deprecated && skipDeprecated)) .map(child => factory.createTypeReferenceNode( @@ -309,11 +308,7 @@ export class Class { ); // A type can have a valid typedef, add that if so. - children.push( - ...this.typedefs.map(t => - factory.createTypeReferenceNode(t, /*typeArgs=*/ []) - ) - ); + children.push(...this.typedefs); const upRef = this.leafName() || this.baseName(); return upRef @@ -379,9 +374,25 @@ export class Builtin extends Class {} * in JSON-LD and JavaScript as a typedef to a native type. */ export class AliasBuiltin extends Builtin { - constructor(url: string, equivTo: string) { + constructor(url: string, ...equivTo: TypeNode[]) { super(UrlNode.Parse(url)); - this.addTypedef(equivTo); + for (const t of equivTo) this.addTypedef(t); + } + + static Alias(equivTo: string): TypeNode { + return factory.createTypeReferenceNode(equivTo, /*typeArgs=*/ []); + } + + static NumberStringLiteral(): TypeNode { + return factory.createTemplateLiteralType( + factory.createTemplateHead(/* text= */ ''), + [ + factory.createTemplateLiteralTypeSpan( + factory.createTypeReferenceNode('number'), + factory.createTemplateTail(/* text= */ '') + ), + ] + ); } } diff --git a/test/baselines/comments_test.ts b/test/baselines/comments_test.ts index 8c92de8..bc0c0b8 100644 --- a/test/baselines/comments_test.ts +++ b/test/baselines/comments_test.ts @@ -86,7 +86,7 @@ type IdReference = { * - Use values from 0123456789 (Unicode 'DIGIT ZERO' (U+0030) to 'DIGIT NINE' (U+0039)) rather than superficially similiar Unicode symbols. * - Use '.' (Unicode 'FULL STOP' (U+002E)) rather than ',' to indicate a decimal point. Avoid using these symbols as a readability separator. */ -export type Number = number; +export type Number = number | \`\${number}\`; /** Data type: Text. */ export type Text = string; diff --git a/test/baselines/data_type_union_test.ts b/test/baselines/data_type_union_test.ts index 8fa7110..ec1f3e9 100644 --- a/test/baselines/data_type_union_test.ts +++ b/test/baselines/data_type_union_test.ts @@ -57,7 +57,7 @@ type IdReference = { \\"@id\\": string; }; -export type Number = number; +export type Number = number | \`\${number}\`; export type Text = string; diff --git a/test/baselines/deprecated_objects_test.ts b/test/baselines/deprecated_objects_test.ts index 15f9651..cd62114 100644 --- a/test/baselines/deprecated_objects_test.ts +++ b/test/baselines/deprecated_objects_test.ts @@ -80,7 +80,7 @@ type IdReference = { \\"@id\\": string; }; -export type Number = number; +export type Number = number | \`\${number}\`; export type Text = string; diff --git a/test/baselines/inheritance_multiple_test.ts b/test/baselines/inheritance_multiple_test.ts index a77c722..2226cbd 100644 --- a/test/baselines/inheritance_multiple_test.ts +++ b/test/baselines/inheritance_multiple_test.ts @@ -61,7 +61,7 @@ type IdReference = { \\"@id\\": string; }; -export type Number = number; +export type Number = number | \`\${number}\`; export type Text = string; diff --git a/test/baselines/inheritance_one_test.ts b/test/baselines/inheritance_one_test.ts index e7d63d5..7598124 100644 --- a/test/baselines/inheritance_one_test.ts +++ b/test/baselines/inheritance_one_test.ts @@ -56,7 +56,7 @@ type IdReference = { \\"@id\\": string; }; -export type Number = number; +export type Number = number | \`\${number}\`; export type Text = string; diff --git a/test/baselines/nodeprecated_objects_test.ts b/test/baselines/nodeprecated_objects_test.ts index 0ce1235..757663e 100644 --- a/test/baselines/nodeprecated_objects_test.ts +++ b/test/baselines/nodeprecated_objects_test.ts @@ -79,7 +79,7 @@ type IdReference = { \\"@id\\": string; }; -export type Number = number; +export type Number = number | \`\${number}\`; export type Text = string; diff --git a/test/baselines/sorted_proptypes_test.ts b/test/baselines/sorted_proptypes_test.ts index fce4b7b..495f83e 100644 --- a/test/baselines/sorted_proptypes_test.ts +++ b/test/baselines/sorted_proptypes_test.ts @@ -72,7 +72,7 @@ export type Date = string; export type DateTime = string; -export type Number = number; +export type Number = number | \`\${number}\`; export type Text = string; diff --git a/test/ts/class_test.ts b/test/ts/class_test.ts index 2accbe9..9b1016f 100644 --- a/test/ts/class_test.ts +++ b/test/ts/class_test.ts @@ -70,14 +70,6 @@ describe('Class', () => { }); }); - it("can't add typedef twice", () => { - const cls = makeClass('https://schema.org/Person'); - cls.addTypedef('string'); - expect(() => cls.addTypedef('Foo')).toThrowError( - 'already has typedef string' - ); - }); - describe('toNode', () => { it('by default (no parent)', () => { // A class with no parent has a top-level "@id" @@ -309,28 +301,40 @@ describe('Sort(Class, Class)', () => { // Before regular classes. expect( Sort( - new AliasBuiltin('https://schema.org/Text', 'string'), + new AliasBuiltin( + 'https://schema.org/Text', + AliasBuiltin.Alias('string') + ), makeClass('https://schema.org/A') ) ).toBe(-1); expect( Sort( makeClass('https://schema.org/A'), - new AliasBuiltin('https://schema.org/Text', 'string') + new AliasBuiltin( + 'https://schema.org/Text', + AliasBuiltin.Alias('string') + ) ) ).toBe(+1); // Before regular classes with different domains. expect( Sort( - new AliasBuiltin('https://schema.org/Text', 'string'), + new AliasBuiltin( + 'https://schema.org/Text', + AliasBuiltin.Alias('string') + ), makeClass('https://a.org/DataType') ) ).toBe(-1); expect( Sort( makeClass('https://a.org/DataType'), - new AliasBuiltin('https://schema.org/Text', 'string') + new AliasBuiltin( + 'https://schema.org/Text', + AliasBuiltin.Alias('string') + ) ) ).toBe(+1); @@ -338,18 +342,24 @@ describe('Sort(Class, Class)', () => { expect( Sort( new DataTypeUnion('https://schema.org/DataType', []), - new AliasBuiltin('https://schema.org/A', 'string') + new AliasBuiltin('https://schema.org/A', AliasBuiltin.Alias('string')) ) ).toBe(+1); expect( Sort( - new AliasBuiltin('https://schema.org/A', 'string'), + new AliasBuiltin( + 'https://schema.org/A', + AliasBuiltin.Alias('string') + ), new DataTypeUnion('https://schema.org/DataType', []) ) ).toBe(-1); expect( Sort( - new AliasBuiltin('https://schema.org/Z', 'string'), + new AliasBuiltin( + 'https://schema.org/Z', + AliasBuiltin.Alias('string') + ), new DataTypeUnion('https://schema.org/DataType', []) ) ).toBe(-1); @@ -358,48 +368,60 @@ describe('Sort(Class, Class)', () => { expect( Sort( new Builtin(UrlNode.Parse('https://schema.org/Boo')), - new AliasBuiltin('https://schema.org/Boo', 'Text') + new AliasBuiltin('https://schema.org/Boo', AliasBuiltin.Alias('Text')) ) ).toBe(0); // Sorts within Builtins expect( Sort( - new AliasBuiltin('https://schema.org/A', 'string'), - new AliasBuiltin('https://schema.org/B', 'string') + new AliasBuiltin( + 'https://schema.org/A', + AliasBuiltin.Alias('string') + ), + new AliasBuiltin('https://schema.org/B', AliasBuiltin.Alias('string')) ) ).toBe(-1); expect( Sort( - new AliasBuiltin('https://schema.org/B', 'string'), - new AliasBuiltin('https://schema.org/A', 'string') + new AliasBuiltin( + 'https://schema.org/B', + AliasBuiltin.Alias('string') + ), + new AliasBuiltin('https://schema.org/A', AliasBuiltin.Alias('string')) ) ).toBe(+1); expect( Sort( - new AliasBuiltin('https://schema.org/C', 'string'), - new AliasBuiltin('https://schema.org/C', 'string') + new AliasBuiltin( + 'https://schema.org/C', + AliasBuiltin.Alias('string') + ), + new AliasBuiltin('https://schema.org/C', AliasBuiltin.Alias('string')) ) ).toBe(0); expect( Sort( - new AliasBuiltin('https://schema.org/A#Z', 'string'), - new AliasBuiltin('https://schema.org/C', 'string') + new AliasBuiltin( + 'https://schema.org/A#Z', + AliasBuiltin.Alias('string') + ), + new AliasBuiltin('https://schema.org/C', AliasBuiltin.Alias('string')) ) ).toBe(+1); expect( Sort( - new AliasBuiltin('https://z.org/C', 'string'), - new AliasBuiltin('https://schema.org/C', 'string') + new AliasBuiltin('https://z.org/C', AliasBuiltin.Alias('string')), + new AliasBuiltin('https://schema.org/C', AliasBuiltin.Alias('string')) ) ).toBe(+1); expect( Sort( - new AliasBuiltin('https://z.org/Z#A', 'string'), - new AliasBuiltin('https://schema.org/C', 'string') + new AliasBuiltin('https://z.org/Z#A', AliasBuiltin.Alias('string')), + new AliasBuiltin('https://schema.org/C', AliasBuiltin.Alias('string')) ) ).toBe(-1); });