From 3a3a3aea31e42de0f6e23245b3a0042df111d424 Mon Sep 17 00:00:00 2001 From: Evan Sosenko Date: Mon, 9 Sep 2024 14:36:33 -0700 Subject: [PATCH 1/5] Set path Zod parse to improve error message --- src/lib/blueprint.test.ts | 2 ++ src/lib/blueprint.ts | 42 ++++++++++++++++++++++++++++----------- 2 files changed, 32 insertions(+), 12 deletions(-) diff --git a/src/lib/blueprint.test.ts b/src/lib/blueprint.test.ts index 7c51fb1..b5a0c03 100644 --- a/src/lib/blueprint.test.ts +++ b/src/lib/blueprint.test.ts @@ -17,6 +17,7 @@ test('createProperties: assigns appropriate default values', (t) => { const properties = createProperties( minimalProperties as Record, + 'foo', ) t.is(properties.length, 1, 'Should create one property') @@ -49,6 +50,7 @@ test('createProperties: uses provided values', (t) => { const properties = createProperties( fullProperties as Record, + 'foo', ) t.is(properties.length, 1, 'Should create one property') diff --git a/src/lib/blueprint.ts b/src/lib/blueprint.ts index b8c70a8..36bca95 100644 --- a/src/lib/blueprint.ts +++ b/src/lib/blueprint.ts @@ -361,7 +361,9 @@ const createEndpoint = async ( throw new Error(`Could not resolve name for endpoint at ${path}`) } - const parsedOperation = OpenapiOperationSchema.parse(operation) + const parsedOperation = OpenapiOperationSchema.parse(operation, { + path: pathParts, + }) const title = parsedOperation['x-title'] @@ -373,7 +375,7 @@ const createEndpoint = async ( const deprecationMessage = parsedOperation['x-deprecated'] - const request = createRequest(methods, operation) + const request = createRequest(methods, operation, path) const endpoint: Omit = { title, @@ -383,7 +385,7 @@ const createEndpoint = async ( isUndocumented, isDeprecated, deprecationMessage, - response: createResponse(operation), + response: createResponse(operation, path), request, } @@ -406,6 +408,7 @@ const createEndpoint = async ( export const createRequest = ( methods: Method[], operation: OpenapiOperation, + path: string, ): Request => { if (methods.length === 0) { // eslint-disable-next-line no-console @@ -429,11 +432,14 @@ export const createRequest = ( methods, semanticMethod, preferredMethod, - parameters: createRequestBody(operation), + parameters: createRequestBody(operation, path), } } -const createRequestBody = (operation: OpenapiOperation): Parameter[] => { +const createRequestBody = ( + operation: OpenapiOperation, + path: string, +): Parameter[] => { // This should be done by the createParameters but for some reason it's not // TODO: remove this in favour of using createParameters if (!('requestBody' in operation) || operation.requestBody === undefined) { @@ -453,16 +459,19 @@ const createRequestBody = (operation: OpenapiOperation): Parameter[] => { return [] } - return createParameters(schema.properties, schema.required) + return createParameters(schema.properties, path, schema.required) } const createParameters = ( properties: Record, + path: string, requiredParameters: string[] = [], ): Parameter[] => { return Object.entries(properties).map( ([name, property]: [string, any]): Parameter => { - const parsedProp = PropertySchema.parse(property) + const parsedProp = PropertySchema.parse(property, { + path: [...path.split('/'), name], + }) const baseParam: BaseParameter = { name, @@ -504,6 +513,7 @@ const createParameters = ( jsonType: 'object', parameters: createParameters( property.properties as Record, + path, ), } } @@ -540,7 +550,7 @@ const createResources = ( ...acc, [schemaName]: { resourceType: schemaName, - properties: createProperties(schema.properties), + properties: createProperties(schema.properties, schemaName), description: schema.description ?? '', }, } @@ -549,14 +559,19 @@ const createResources = ( }, {}) } -const createResponse = (operation: OpenapiOperation): Response => { +const createResponse = ( + operation: OpenapiOperation, + path: string, +): Response => { if (!('responses' in operation) || operation.responses == null) { throw new Error( `Missing responses in operation for ${operation.operationId}`, ) } - const parsedOperation = OpenapiOperationSchema.parse(operation) + const parsedOperation = OpenapiOperationSchema.parse(operation, { + path: [...path.split('/')], + }) const { responses } = operation const okResponse = responses['200'] @@ -632,9 +647,12 @@ const createResponse = (operation: OpenapiOperation): Response => { export const createProperties = ( properties: Record, + resourceType: string, ): Property[] => { return Object.entries(properties).map(([name, prop]): Property => { - const parsedProp = PropertySchema.parse(prop) + const parsedProp = PropertySchema.parse(prop, { + path: [resourceType, name], + }) const baseProperty = { name, @@ -671,7 +689,7 @@ export const createProperties = ( ...baseProperty, format: 'object', jsonType: 'object', - properties: createProperties(prop.properties), + properties: createProperties(prop.properties, resourceType), } } return { ...baseProperty, format: 'record', jsonType: 'object' } From efa220e06b5a7b0b54e5127bcc251ef5b1edaf06 Mon Sep 17 00:00:00 2001 From: Evan Sosenko Date: Mon, 9 Sep 2024 14:44:12 -0700 Subject: [PATCH 2/5] Update @seamapi/types --- package-lock.json | 9 +++++---- package.json | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5c1584c..f2b8159 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,7 @@ "zod": "^3.23.8" }, "devDependencies": { - "@seamapi/types": "1.228.0", + "@seamapi/types": "1.240.0", "@types/node": "^20.8.10", "ava": "^6.0.1", "c8": "^10.1.2", @@ -1052,10 +1052,11 @@ ] }, "node_modules/@seamapi/types": { - "version": "1.228.0", - "resolved": "https://registry.npmjs.org/@seamapi/types/-/types-1.228.0.tgz", - "integrity": "sha512-p7ZKWMdKp36QIAA7ta3qa0Z1qzXcTpyzAU9rw2IptES5oI8wwpR5hxK+VzdccJW4/Z3kT/9+K3RxPhUIgEB0jQ==", + "version": "1.240.0", + "resolved": "https://registry.npmjs.org/@seamapi/types/-/types-1.240.0.tgz", + "integrity": "sha512-Yebt0M/92SgmNEjQrvPvc2NchdYqubcqUtTdYaTpoqGC3vpp76U0Q9D61aFtxKxAZ7Davc/H+EXhjkr64Im6RQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=18.12.0", "npm": ">= 9.0.0" diff --git a/package.json b/package.json index 122f30f..4694c46 100644 --- a/package.json +++ b/package.json @@ -73,7 +73,7 @@ "zod": "^3.23.8" }, "devDependencies": { - "@seamapi/types": "1.228.0", + "@seamapi/types": "1.240.0", "@types/node": "^20.8.10", "ava": "^6.0.1", "c8": "^10.1.2", From 244a736ac81276b9d5ce7af01c4d2b50e6bb2036 Mon Sep 17 00:00:00 2001 From: Evan Sosenko Date: Mon, 9 Sep 2024 14:58:26 -0700 Subject: [PATCH 3/5] Filter out undefined property types --- src/lib/blueprint.ts | 114 +++++--- src/lib/openapi-schema.ts | 2 +- test/snapshots/seam-blueprint.test.ts.md | 324 +++++++++++++++++++++ test/snapshots/seam-blueprint.test.ts.snap | Bin 19347 -> 21096 bytes 4 files changed, 392 insertions(+), 48 deletions(-) diff --git a/src/lib/blueprint.ts b/src/lib/blueprint.ts index 36bca95..4a4db39 100644 --- a/src/lib/blueprint.ts +++ b/src/lib/blueprint.ts @@ -467,8 +467,18 @@ const createParameters = ( path: string, requiredParameters: string[] = [], ): Parameter[] => { - return Object.entries(properties).map( - ([name, property]: [string, any]): Parameter => { + return Object.entries(properties) + .filter(([name, property]) => { + if (property.type == null) { + // eslint-disable-next-line no-console + console.warn( + `The ${name} property for ${path} will not be documented since it does not define a type.`, + ) + return false + } + return true + }) + .map(([name, property]: [string, any]): Parameter => { const parsedProp = PropertySchema.parse(property, { path: [...path.split('/'), name], }) @@ -527,8 +537,7 @@ const createParameters = ( default: throw new Error(`Unsupported property type: ${parsedProp.type}`) } - }, - ) + }) } const createResources = ( @@ -649,54 +658,65 @@ export const createProperties = ( properties: Record, resourceType: string, ): Property[] => { - return Object.entries(properties).map(([name, prop]): Property => { - const parsedProp = PropertySchema.parse(prop, { - path: [resourceType, name], + return Object.entries(properties) + .filter(([name, property]) => { + if (property.type == null) { + // eslint-disable-next-line no-console + console.warn( + `The ${name} property for ${resourceType} will not be documented since it does not define a type.`, + ) + return false + } + return true }) + .map(([name, prop]): Property => { + const parsedProp = PropertySchema.parse(prop, { + path: [resourceType, name], + }) - const baseProperty = { - name, - description: parsedProp.description, - isDeprecated: parsedProp['x-deprecated'].length > 0, - deprecationMessage: parsedProp['x-deprecated'], - isUndocumented: parsedProp['x-undocumented'].length > 0, - } + const baseProperty = { + name, + description: parsedProp.description, + isDeprecated: parsedProp['x-deprecated'].length > 0, + deprecationMessage: parsedProp['x-deprecated'], + isUndocumented: parsedProp['x-undocumented'].length > 0, + } - switch (parsedProp.type) { - case 'string': - if (parsedProp.enum !== undefined) { - return { - ...baseProperty, - format: 'enum', - jsonType: 'string', - values: parsedProp.enum.map((value: any) => ({ name: value })), + switch (parsedProp.type) { + case 'string': + if (parsedProp.enum !== undefined) { + return { + ...baseProperty, + format: 'enum', + jsonType: 'string', + values: parsedProp.enum.map((value: any) => ({ name: value })), + } } - } - if (parsedProp.format === 'date-time') { - return { ...baseProperty, format: 'datetime', jsonType: 'string' } - } - if (parsedProp.format === 'uuid') { - return { ...baseProperty, format: 'id', jsonType: 'string' } - } - return { ...baseProperty, format: 'string', jsonType: 'string' } - case 'boolean': - return { ...baseProperty, format: 'boolean', jsonType: 'boolean' } - case 'array': - return { ...baseProperty, format: 'list', jsonType: 'array' } - case 'object': - if (prop.properties !== undefined) { - return { - ...baseProperty, - format: 'object', - jsonType: 'object', - properties: createProperties(prop.properties, resourceType), + if (parsedProp.format === 'date-time') { + return { ...baseProperty, format: 'datetime', jsonType: 'string' } } - } - return { ...baseProperty, format: 'record', jsonType: 'object' } - default: - throw new Error(`Unsupported property type: ${parsedProp.type}`) - } - }) + if (parsedProp.format === 'uuid') { + return { ...baseProperty, format: 'id', jsonType: 'string' } + } + return { ...baseProperty, format: 'string', jsonType: 'string' } + case 'boolean': + return { ...baseProperty, format: 'boolean', jsonType: 'boolean' } + case 'array': + return { ...baseProperty, format: 'list', jsonType: 'array' } + case 'object': + if (prop.properties !== undefined) { + return { + ...baseProperty, + format: 'object', + jsonType: 'object', + properties: createProperties(prop.properties, resourceType), + } + } + return { ...baseProperty, format: 'record', jsonType: 'object' } + default: + throw new Error(`Unsupported property type: ${parsedProp.type}`) + } + }) } export const getSemanticMethod = (methods: Method[]): Method => { diff --git a/src/lib/openapi-schema.ts b/src/lib/openapi-schema.ts index 99a2439..4a830a4 100644 --- a/src/lib/openapi-schema.ts +++ b/src/lib/openapi-schema.ts @@ -71,7 +71,7 @@ export const PropertySchema: z.ZodSchema = z.object({ deprecated: z.boolean().default(false), 'x-undocumented': z.string().default(''), 'x-deprecated': z.string().default(''), - enum: z.array(z.string()).optional(), + enum: z.array(z.string().or(z.boolean())).optional(), $ref: z.string().optional(), format: z.string().optional(), }) diff --git a/test/snapshots/seam-blueprint.test.ts.md b/test/snapshots/seam-blueprint.test.ts.md index 0c5d28d..5fd409a 100644 --- a/test/snapshots/seam-blueprint.test.ts.md +++ b/test/snapshots/seam-blueprint.test.ts.md @@ -113,6 +113,15 @@ Generated by [AVA](https://avajs.dev). jsonType: 'string', name: 'external_type_display_name', }, + { + deprecationMessage: '', + description: '', + format: 'boolean', + isDeprecated: false, + isUndocumented: false, + jsonType: 'boolean', + name: 'is_managed', + }, { deprecationMessage: '', description: '', @@ -259,6 +268,9 @@ Generated by [AVA](https://avajs.dev). { name: 'visionline_card', }, + { + name: 'salto_ks_credential', + }, ], }, { @@ -279,6 +291,15 @@ Generated by [AVA](https://avajs.dev). jsonType: 'boolean', name: 'is_latest_desired_state_synced_with_provider', }, + { + deprecationMessage: '', + description: '', + format: 'boolean', + isDeprecated: false, + isUndocumented: false, + jsonType: 'boolean', + name: 'is_managed', + }, { deprecationMessage: '', description: '', @@ -341,6 +362,15 @@ Generated by [AVA](https://avajs.dev). }, ], }, + { + deprecationMessage: '', + description: '', + format: 'string', + isDeprecated: false, + isUndocumented: false, + jsonType: 'string', + name: 'card_id', + }, { deprecationMessage: '', description: '', @@ -350,6 +380,15 @@ Generated by [AVA](https://avajs.dev). jsonType: 'array', name: 'common_acs_entrance_ids', }, + { + deprecationMessage: '', + description: '', + format: 'string', + isDeprecated: false, + isUndocumented: false, + jsonType: 'string', + name: 'credential_id', + }, { deprecationMessage: '', description: '', @@ -359,6 +398,15 @@ Generated by [AVA](https://avajs.dev). jsonType: 'array', name: 'guest_acs_entrance_ids', }, + { + deprecationMessage: '', + description: '', + format: 'boolean', + isDeprecated: false, + isUndocumented: false, + jsonType: 'boolean', + name: 'is_valid', + }, { deprecationMessage: '', description: '', @@ -1066,6 +1114,15 @@ Generated by [AVA](https://avajs.dev). jsonType: 'boolean', name: 'is_latest_desired_state_synced_with_provider', }, + { + deprecationMessage: '', + description: '', + format: 'boolean', + isDeprecated: false, + isUndocumented: false, + jsonType: 'boolean', + name: 'is_managed', + }, { deprecationMessage: '', description: '', @@ -1433,6 +1490,92 @@ Generated by [AVA](https://avajs.dev). path: '/acs/access_groups', subroutes: [], }, + { + endpoints: [ + { + codeSamples: [], + deprecationMessage: '', + description: '', + isDeprecated: false, + isUndocumented: false, + name: 'get', + path: '/acs/access_groups/unmanaged/get', + request: { + methods: [ + 'POST', + ], + parameters: [ + { + deprecationMessage: '', + description: '', + format: 'id', + isDeprecated: false, + isRequired: true, + isUndocumented: false, + jsonType: 'string', + name: 'acs_access_group_id', + }, + ], + preferredMethod: 'POST', + semanticMethod: 'POST', + }, + response: { + description: 'OK', + responseType: 'void', + }, + title: '', + }, + { + codeSamples: [], + deprecationMessage: '', + description: '', + isDeprecated: false, + isUndocumented: false, + name: 'list', + path: '/acs/access_groups/unmanaged/list', + request: { + methods: [ + 'POST', + ], + parameters: [ + { + deprecationMessage: '', + description: '', + format: 'id', + isDeprecated: false, + isRequired: false, + isUndocumented: false, + jsonType: 'string', + name: 'acs_system_id', + }, + { + deprecationMessage: '', + description: '', + format: 'id', + isDeprecated: false, + isRequired: false, + isUndocumented: false, + jsonType: 'string', + name: 'acs_user_id', + }, + ], + preferredMethod: 'POST', + semanticMethod: 'POST', + }, + response: { + description: 'OK', + responseType: 'void', + }, + title: '', + }, + ], + name: 'unmanaged', + namespace: { + path: '/acs', + }, + path: '/acs/access_groups/unmanaged', + subroutes: [], + }, { endpoints: [ { @@ -2170,6 +2313,71 @@ Generated by [AVA](https://avajs.dev). path: '/acs/credentials', subroutes: [], }, + { + endpoints: [ + { + codeSamples: [], + deprecationMessage: '', + description: '', + isDeprecated: false, + isUndocumented: false, + name: 'get', + path: '/acs/credentials/unmanaged/get', + request: { + methods: [ + 'POST', + ], + parameters: [ + { + deprecationMessage: '', + description: '', + format: 'id', + isDeprecated: false, + isRequired: true, + isUndocumented: false, + jsonType: 'string', + name: 'acs_credential_id', + }, + ], + preferredMethod: 'POST', + semanticMethod: 'POST', + }, + response: { + description: 'OK', + responseType: 'void', + }, + title: '', + }, + { + codeSamples: [], + deprecationMessage: '', + description: '', + isDeprecated: false, + isUndocumented: false, + name: 'list', + path: '/acs/credentials/unmanaged/list', + request: { + methods: [ + 'POST', + ], + parameters: [], + preferredMethod: 'POST', + semanticMethod: 'POST', + }, + response: { + description: 'OK', + responseType: 'void', + }, + title: '', + }, + ], + name: 'unmanaged', + namespace: { + path: '/acs', + }, + path: '/acs/credentials/unmanaged', + subroutes: [], + }, { endpoints: [ { @@ -3242,6 +3450,122 @@ Generated by [AVA](https://avajs.dev). path: '/acs/users', subroutes: [], }, + { + endpoints: [ + { + codeSamples: [], + deprecationMessage: '', + description: '', + isDeprecated: false, + isUndocumented: false, + name: 'get', + path: '/acs/users/unmanaged/get', + request: { + methods: [ + 'POST', + ], + parameters: [ + { + deprecationMessage: '', + description: '', + format: 'id', + isDeprecated: false, + isRequired: true, + isUndocumented: false, + jsonType: 'string', + name: 'acs_user_id', + }, + ], + preferredMethod: 'POST', + semanticMethod: 'POST', + }, + response: { + description: 'OK', + responseType: 'void', + }, + title: '', + }, + { + codeSamples: [], + deprecationMessage: '', + description: '', + isDeprecated: false, + isUndocumented: false, + name: 'list', + path: '/acs/users/unmanaged/list', + request: { + methods: [ + 'POST', + ], + parameters: [ + { + deprecationMessage: '', + description: '', + format: 'id', + isDeprecated: false, + isRequired: false, + isUndocumented: false, + jsonType: 'string', + name: 'acs_system_id', + }, + { + deprecationMessage: '', + description: '', + format: 'number', + isDeprecated: false, + isRequired: false, + isUndocumented: false, + jsonType: 'number', + name: 'limit', + }, + { + deprecationMessage: '', + description: '', + format: 'string', + isDeprecated: false, + isRequired: false, + isUndocumented: false, + jsonType: 'string', + name: 'user_identity_email_address', + }, + { + deprecationMessage: '', + description: '', + format: 'id', + isDeprecated: false, + isRequired: false, + isUndocumented: false, + jsonType: 'string', + name: 'user_identity_id', + }, + { + deprecationMessage: '', + description: '', + format: 'string', + isDeprecated: false, + isRequired: false, + isUndocumented: false, + jsonType: 'string', + name: 'user_identity_phone_number', + }, + ], + preferredMethod: 'POST', + semanticMethod: 'POST', + }, + response: { + description: 'OK', + responseType: 'void', + }, + title: '', + }, + ], + name: 'unmanaged', + namespace: { + path: '/acs', + }, + path: '/acs/users/unmanaged', + subroutes: [], + }, ], title: 'Seam Connect', } diff --git a/test/snapshots/seam-blueprint.test.ts.snap b/test/snapshots/seam-blueprint.test.ts.snap index d6e606f5a4f118eb279f35327e43f3a36ff9b9dc..bd223fc0521b899aec33e8301a6fbf7c2826058b 100644 GIT binary patch literal 21096 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 q=KgzDY7^*tM#T92>l<`Uo#>D`T+oJNtE&c From 1e5f1da3f26c75f9eaf8ce472076d0eb43af4625 Mon Sep 17 00:00:00 2001 From: Evan Sosenko Date: Mon, 9 Sep 2024 15:05:38 -0700 Subject: [PATCH 4/5] Pass down parentPaths --- src/lib/blueprint.test.ts | 4 +- src/lib/blueprint.ts | 102 ++++++++++++++++++++------------------ 2 files changed, 56 insertions(+), 50 deletions(-) diff --git a/src/lib/blueprint.test.ts b/src/lib/blueprint.test.ts index b5a0c03..822e1fe 100644 --- a/src/lib/blueprint.test.ts +++ b/src/lib/blueprint.test.ts @@ -17,7 +17,7 @@ test('createProperties: assigns appropriate default values', (t) => { const properties = createProperties( minimalProperties as Record, - 'foo', + ['foo'], ) t.is(properties.length, 1, 'Should create one property') @@ -50,7 +50,7 @@ test('createProperties: uses provided values', (t) => { const properties = createProperties( fullProperties as Record, - 'foo', + ['foo'], ) t.is(properties.length, 1, 'Should create one property') diff --git a/src/lib/blueprint.ts b/src/lib/blueprint.ts index 4a4db39..cd27263 100644 --- a/src/lib/blueprint.ts +++ b/src/lib/blueprint.ts @@ -559,7 +559,7 @@ const createResources = ( ...acc, [schemaName]: { resourceType: schemaName, - properties: createProperties(schema.properties, schemaName), + properties: createProperties(schema.properties, [schemaName]), description: schema.description ?? '', }, } @@ -656,67 +656,73 @@ const createResponse = ( export const createProperties = ( properties: Record, - resourceType: string, + parentPaths: string[], ): Property[] => { return Object.entries(properties) .filter(([name, property]) => { if (property.type == null) { // eslint-disable-next-line no-console console.warn( - `The ${name} property for ${resourceType} will not be documented since it does not define a type.`, + `The ${name} property for ${parentPaths.join('.')} will not be documented since it does not define a type.`, ) return false } return true }) - .map(([name, prop]): Property => { - const parsedProp = PropertySchema.parse(prop, { - path: [resourceType, name], - }) + .map(([name, prop]) => createProperty(name, prop, [...parentPaths])) +} - const baseProperty = { - name, - description: parsedProp.description, - isDeprecated: parsedProp['x-deprecated'].length > 0, - deprecationMessage: parsedProp['x-deprecated'], - isUndocumented: parsedProp['x-undocumented'].length > 0, - } +const createProperty = ( + name: string, + prop: OpenapiSchema, + parentPaths: string[], +): Property => { + const parsedProp = PropertySchema.parse(prop, { + path: [...parentPaths, name], + }) - switch (parsedProp.type) { - case 'string': - if (parsedProp.enum !== undefined) { - return { - ...baseProperty, - format: 'enum', - jsonType: 'string', - values: parsedProp.enum.map((value: any) => ({ name: value })), - } - } - if (parsedProp.format === 'date-time') { - return { ...baseProperty, format: 'datetime', jsonType: 'string' } - } - if (parsedProp.format === 'uuid') { - return { ...baseProperty, format: 'id', jsonType: 'string' } - } - return { ...baseProperty, format: 'string', jsonType: 'string' } - case 'boolean': - return { ...baseProperty, format: 'boolean', jsonType: 'boolean' } - case 'array': - return { ...baseProperty, format: 'list', jsonType: 'array' } - case 'object': - if (prop.properties !== undefined) { - return { - ...baseProperty, - format: 'object', - jsonType: 'object', - properties: createProperties(prop.properties, resourceType), - } - } - return { ...baseProperty, format: 'record', jsonType: 'object' } - default: - throw new Error(`Unsupported property type: ${parsedProp.type}`) + const baseProperty = { + name, + description: parsedProp.description, + isDeprecated: parsedProp['x-deprecated'].length > 0, + deprecationMessage: parsedProp['x-deprecated'], + isUndocumented: parsedProp['x-undocumented'].length > 0, + } + + switch (parsedProp.type) { + case 'string': + if (parsedProp.enum !== undefined) { + return { + ...baseProperty, + format: 'enum', + jsonType: 'string', + values: parsedProp.enum.map((value: any) => ({ name: value })), + } } - }) + if (parsedProp.format === 'date-time') { + return { ...baseProperty, format: 'datetime', jsonType: 'string' } + } + if (parsedProp.format === 'uuid') { + return { ...baseProperty, format: 'id', jsonType: 'string' } + } + return { ...baseProperty, format: 'string', jsonType: 'string' } + case 'boolean': + return { ...baseProperty, format: 'boolean', jsonType: 'boolean' } + case 'array': + return { ...baseProperty, format: 'list', jsonType: 'array' } + case 'object': + if (prop.properties !== undefined) { + return { + ...baseProperty, + format: 'object', + jsonType: 'object', + properties: createProperties(prop.properties, [...parentPaths, name]), + } + } + return { ...baseProperty, format: 'record', jsonType: 'object' } + default: + throw new Error(`Unsupported property type: ${parsedProp.type}`) + } } export const getSemanticMethod = (methods: Method[]): Method => { From 7a8a356ad374864da7a832f96947e902e9e07d20 Mon Sep 17 00:00:00 2001 From: Evan Sosenko Date: Mon, 9 Sep 2024 15:09:56 -0700 Subject: [PATCH 5/5] Split createParameter from createParameters --- src/lib/blueprint.ts | 126 +++++++++++++++++++++++-------------------- 1 file changed, 68 insertions(+), 58 deletions(-) diff --git a/src/lib/blueprint.ts b/src/lib/blueprint.ts index cd27263..629a2fa 100644 --- a/src/lib/blueprint.ts +++ b/src/lib/blueprint.ts @@ -478,66 +478,76 @@ const createParameters = ( } return true }) - .map(([name, property]: [string, any]): Parameter => { - const parsedProp = PropertySchema.parse(property, { - path: [...path.split('/'), name], - }) - - const baseParam: BaseParameter = { - name, - description: parsedProp.description, - isRequired: requiredParameters.includes(name), - isDeprecated: parsedProp['x-deprecated'].length > 0, - deprecationMessage: parsedProp['x-deprecated'], - isUndocumented: parsedProp['x-undocumented'].length > 0, - } + .map( + ([name, property]: [string, any]): Parameter => + createParameter(name, property, path, requiredParameters), + ) +} + +const createParameter = ( + name: string, + property: any, + path: string, + requiredParameters: string[], +): Parameter => { + const parsedProp = PropertySchema.parse(property, { + path: [...path.split('/'), name], + }) + + const baseParam: BaseParameter = { + name, + description: parsedProp.description, + isRequired: requiredParameters.includes(name), + isDeprecated: parsedProp['x-deprecated'].length > 0, + deprecationMessage: parsedProp['x-deprecated'], + isUndocumented: parsedProp['x-undocumented'].length > 0, + } - switch (parsedProp.type) { - case 'string': - if (parsedProp.enum !== undefined) { - return { - ...baseParam, - format: 'enum', - jsonType: 'string', - values: parsedProp.enum.map((value: any) => ({ - name: value, - })), - } - } - if (parsedProp.format === 'date-time') { - return { ...baseParam, format: 'datetime', jsonType: 'string' } - } - if (parsedProp.format === 'uuid') { - return { ...baseParam, format: 'id', jsonType: 'string' } - } - return { ...baseParam, format: 'string', jsonType: 'string' } - case 'boolean': - return { ...baseParam, format: 'boolean', jsonType: 'boolean' } - case 'array': - return { ...baseParam, format: 'list', jsonType: 'array' } - case 'object': - if (property.properties !== undefined) { - return { - ...baseParam, - format: 'object', - jsonType: 'object', - parameters: createParameters( - property.properties as Record, - path, - ), - } - } - return { ...baseParam, format: 'record', jsonType: 'object' } - case 'number': - return { - ...baseParam, - format: 'number', - jsonType: 'number', - } - default: - throw new Error(`Unsupported property type: ${parsedProp.type}`) + switch (parsedProp.type) { + case 'string': + if (parsedProp.enum !== undefined) { + return { + ...baseParam, + format: 'enum', + jsonType: 'string', + values: parsedProp.enum.map((value: any) => ({ + name: value, + })), + } } - }) + if (parsedProp.format === 'date-time') { + return { ...baseParam, format: 'datetime', jsonType: 'string' } + } + if (parsedProp.format === 'uuid') { + return { ...baseParam, format: 'id', jsonType: 'string' } + } + return { ...baseParam, format: 'string', jsonType: 'string' } + case 'boolean': + return { ...baseParam, format: 'boolean', jsonType: 'boolean' } + case 'array': + return { ...baseParam, format: 'list', jsonType: 'array' } + case 'object': + if (property.properties !== undefined) { + return { + ...baseParam, + format: 'object', + jsonType: 'object', + parameters: createParameters( + property.properties as Record, + path, + ), + } + } + return { ...baseParam, format: 'record', jsonType: 'object' } + case 'number': + return { + ...baseParam, + format: 'number', + jsonType: 'number', + } + default: + throw new Error(`Unsupported property type: ${parsedProp.type}`) + } } const createResources = (