From 71873bf16da5d65861d6a69bdf801db0b7993d53 Mon Sep 17 00:00:00 2001 From: Steve Konves Date: Thu, 4 Sep 2025 20:09:54 -0700 Subject: [PATCH 1/5] Reduce package size --- .npmignore | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.npmignore b/.npmignore index 824b66b..3ee7729 100644 --- a/.npmignore +++ b/.npmignore @@ -1,6 +1,10 @@ *.tests.js *.tests.js.map *.tests.d.ts +*.spec.js +*.spec.js.map +*.spec.d.ts src **/snapshot +**/spec /*.* From eee96004b0ae432bab807f04de9489dd15b62a88 Mon Sep 17 00:00:00 2001 From: Steve Konves Date: Thu, 4 Sep 2025 20:10:35 -0700 Subject: [PATCH 2/5] Add property test suite --- .../4.1-structure/4.1.10-property.spec.ts | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 src/spec/4.1-structure/4.1.10-property.spec.ts diff --git a/src/spec/4.1-structure/4.1.10-property.spec.ts b/src/spec/4.1-structure/4.1.10-property.spec.ts new file mode 100644 index 0000000..a83b72b --- /dev/null +++ b/src/spec/4.1-structure/4.1.10-property.spec.ts @@ -0,0 +1,28 @@ +import { expectDefined, parse } from '../test-utils.js'; + +describe('4.1.10 Property', () => { + describe('kind', () => { + it('parses "Property"', async () => { + // ARRANGE + const tsp = ` + import "@typespec/http"; + + @service + namespace Petstore; + + model Widget { + id: string; + } + `; + + // ACT + const { service, violations } = await parse(tsp); + + // ASSERT + expect(violations).toHaveLength(0); + const method = service?.types[0]?.properties[0]; + expectDefined(method); + expect(method.kind).toBe('Property'); + }); + }); +}); From eeec5209648bfb71b792edb28dc5d3eb7a740d13 Mon Sep 17 00:00:00 2001 From: Steve Konves Date: Thu, 4 Sep 2025 20:11:03 -0700 Subject: [PATCH 3/5] Fix bug wherein complex properties were not optional --- src/parser.ts | 1 + .../4.1.13-primitive-value.spec.ts | 37 +++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/src/parser.ts b/src/parser.ts index 868a7cb..6ff88ff 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -526,6 +526,7 @@ export class TypespecParser { kind: 'ComplexValue', typeName: t.name, isArray: options?.asArray ? trueLiteral() : undefined, + isOptional: options?.isOptional ? trueLiteral(loc) : undefined, rules: [], }; case 'ModelProperty': diff --git a/src/spec/4.1-structure/4.1.13-primitive-value.spec.ts b/src/spec/4.1-structure/4.1.13-primitive-value.spec.ts index c4b978b..820a7dd 100644 --- a/src/spec/4.1-structure/4.1.13-primitive-value.spec.ts +++ b/src/spec/4.1-structure/4.1.13-primitive-value.spec.ts @@ -353,6 +353,43 @@ describe('4.1.13 PrimitiveValue', () => { expectDefined(memberValue.isOptional); expect(memberValue.isOptional.value).toBe(true); }); + + it('parses a TrueLiteral for optional complex values', async () => { + // ARRANGE + const tsp = ` + import "@typespec/http"; + + @service + namespace Petstore; + + model Gizmo { + foo: string; + } + + model Widget { + gizmo?: Gizmo; + } + + interface Widgets { + create(widget: Widget): string; + } + `; + + // ACT + const { service, violations } = await parse(tsp); + + // ASSERT + expect(violations).toHaveLength(0); + const widgetType = service?.types.find( + (t) => t.name.value === 'Widget', + ); + expectDefined(widgetType); + + const memberValue = widgetType.properties[0]?.value; + expectDefined(memberValue); + expectDefined(memberValue.isOptional); + expect(memberValue.isOptional.value).toBe(true); + }); }); }); From 22fbe7c0fe2e3e10bf352c9678dbc928c0db5ad5 Mon Sep 17 00:00:00 2001 From: Steve Konves Date: Thu, 4 Sep 2025 20:58:48 -0700 Subject: [PATCH 4/5] Parse interface names as singluar --- package-lock.json | 18 +++++++++++++++++- package.json | 4 +++- src/parser.ts | 12 +++++++++++- src/spec/4.1-structure/4.1.2-interface.spec.ts | 4 ++-- 4 files changed, 33 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6576772..008bc20 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,8 @@ "@typespec/compiler": "^1.3.0", "@typespec/http": "^1.3.0", "@typespec/rest": "^0.73.0", - "basketry": "^0.2.1" + "basketry": "^0.2.1", + "pluralize": "^8.0.0" }, "bin": { "basketry-typespec": "lib/src/rpc.js" @@ -20,6 +21,7 @@ "devDependencies": { "@types/jest": "^30.0.0", "@types/node": "^24.3.0", + "@types/pluralize": "^0.0.33", "@typescript-eslint/eslint-plugin": "^8.41.0", "@typescript-eslint/parser": "^8.41.0", "@typespec/http-server-js": "^0.58.0-alpha.18", @@ -2039,6 +2041,12 @@ "undici-types": "~7.10.0" } }, + "node_modules/@types/pluralize": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/@types/pluralize/-/pluralize-0.0.33.tgz", + "integrity": "sha512-JOqsl+ZoCpP4e8TDke9W79FDcSgPAR0l6pixx2JHkhnRjvShyYiAYw2LVsnA7K08Y6DeOnaU6ujmENO4os/cYg==", + "dev": true + }, "node_modules/@types/stack-utils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", @@ -7133,6 +7141,14 @@ "node": ">=8" } }, + "node_modules/pluralize": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", + "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", + "engines": { + "node": ">=4" + } + }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", diff --git a/package.json b/package.json index a8b5dcb..6477c26 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "devDependencies": { "@types/jest": "^30.0.0", "@types/node": "^24.3.0", + "@types/pluralize": "^0.0.33", "@typescript-eslint/eslint-plugin": "^8.41.0", "@typescript-eslint/parser": "^8.41.0", "@typespec/http-server-js": "^0.58.0-alpha.18", @@ -61,6 +62,7 @@ "@typespec/compiler": "^1.3.0", "@typespec/http": "^1.3.0", "@typespec/rest": "^0.73.0", - "basketry": "^0.2.1" + "basketry": "^0.2.1", + "pluralize": "^8.0.0" } } diff --git a/src/parser.ts b/src/parser.ts index 6ff88ff..7557f9c 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -1,5 +1,6 @@ import * as path from 'path'; import casePkg from 'case'; +import pluralizePkg from 'pluralize'; import * as TSP from '@typespec/compiler'; import { DocNode } from '@typespec/compiler/ast'; @@ -12,6 +13,7 @@ import pkg from '../package.json' with { type: 'json' }; import { decodeRange } from 'basketry'; const { camel, snake } = casePkg; +const { singular } = pluralizePkg; export class TypespecParser { public static async create( @@ -173,9 +175,17 @@ export class TypespecParser { } private parseInterface(int: TSP.Interface): IR.Interface { + const name = this.parseName(int); + + const singularName = { + kind: name.kind, + value: singular(name.value), + loc: name.loc, + }; + return { kind: 'Interface', - name: this.parseName(int), + name: singularName, description: this.parseDescription(int.node?.docs), deprecated: undefined, // TODO: parse interface deprecated methods: this.parseMethods(int), diff --git a/src/spec/4.1-structure/4.1.2-interface.spec.ts b/src/spec/4.1-structure/4.1.2-interface.spec.ts index c19536e..0906bbc 100644 --- a/src/spec/4.1-structure/4.1.2-interface.spec.ts +++ b/src/spec/4.1-structure/4.1.2-interface.spec.ts @@ -25,7 +25,7 @@ describe('4.1.2 Interface', () => { }); describe('name', () => { - it('parses interface name', async () => { + it('parses interface name as singular', async () => { // ARRANGE const tsp = ` import "@typespec/http"; @@ -43,7 +43,7 @@ describe('4.1.2 Interface', () => { expect(violations).toHaveLength(0); const int = service?.interfaces[0]; expectDefined(int); - expect(int.name.value).toBe('Widgets'); + expect(int.name.value).toBe('Widget'); expect(int.name.loc).toBeDefined(); }); }); From 9a8a4c05c066131789ee57c796ef83524f116286 Mon Sep 17 00:00:00 2001 From: Steve Konves Date: Thu, 4 Sep 2025 22:15:46 -0700 Subject: [PATCH 5/5] Parse property descriptions --- src/parser.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/parser.ts b/src/parser.ts index 7557f9c..d08d2db 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -846,7 +846,7 @@ export class TypespecParser { return { kind: 'Property', name: this.parseName(prop), - description: undefined, // TODO: handle description + description: this.parseDescription(prop.node?.docs), value: this.parseMemberValue(prop.type, { isOptional: prop.optional, default: this.parseDefaultValue(prop.defaultValue),