diff --git a/src/lib/blueprint.ts b/src/lib/blueprint.ts index d717b57b..7b3934ed 100644 --- a/src/lib/blueprint.ts +++ b/src/lib/blueprint.ts @@ -193,6 +193,7 @@ interface ResourceResponse extends BaseResponse { responseType: 'resource' responseKey: string resourceType: string + actionAttemptType?: string } interface ResourceListResponse extends BaseResponse { @@ -276,6 +277,7 @@ export type Method = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' interface Context extends Required { codeSampleDefinitions: CodeSampleDefinition[] + actionAttempts: ActionAttempt[] } export const TypesModuleSchema = z.object({ @@ -301,19 +303,21 @@ export const createBlueprint = async ( // TODO: Move openapi to TypesModuleSchema const openapi = typesModule.openapi as Openapi + const resources = createResources(openapi.components.schemas) + const actionAttempts = createActionAttempts(openapi.components.schemas) + const context = { codeSampleDefinitions, formatCode, + actionAttempts, } - const resources = createResources(openapi.components.schemas) - return { title: openapi.info.title, routes: await createRoutes(openapi.paths, context), resources, events: createEvents(openapi.components.schemas, resources), - actionAttempts: createActionAttempts(openapi.components.schemas), + actionAttempts, } } @@ -523,7 +527,7 @@ const createEndpointFromOperation = async ( const draftMessage = parsedOperation['x-draft'] const request = createRequest(methods, operation, path) - const response = createResponse(operation, path) + const response = createResponse(operation, path, context) const operationAuthMethods = parsedOperation.security.map( (securitySchema) => { @@ -843,6 +847,7 @@ const createResource = ( const createResponse = ( operation: OpenapiOperation, path: string, + context: Context, ): Response => { if (!('responses' in operation) || operation.responses == null) { throw new Error( @@ -920,6 +925,12 @@ const createResponse = ( ) } + const actionAttemptType = validateActionAttemptType( + parsedOperation['x-action-attempt-type'], + responseKey, + path, + context, + ) const refKey = responseKey if (refKey != null && properties[refKey] != null) { @@ -931,6 +942,7 @@ const createResponse = ( responseKey: refKey, resourceType: refString?.split('/').at(-1) ?? 'unknown', description, + ...(actionAttemptType != null && { actionAttemptType }), } } } @@ -941,6 +953,37 @@ const createResponse = ( } } +const validateActionAttemptType = ( + actionAttemptType: string | undefined, + responseKey: string, + path: string, + context: Context, +): string | undefined => { + const excludedPaths = ['/action_attempts'] + const isPathExcluded = excludedPaths.some((p) => path.startsWith(p)) + + if ( + actionAttemptType == null && + responseKey === 'action_attempt' && + !isPathExcluded + ) { + throw new Error(`Missing action_attempt_type for path ${path}`) + } + + if ( + actionAttemptType != null && + !context.actionAttempts.some( + (attempt) => attempt.actionAttemptType === actionAttemptType, + ) + ) { + throw new Error( + `Invalid action_attempt_type '${actionAttemptType}' for path ${path}`, + ) + } + + return actionAttemptType +} + export const createProperties = ( properties: Record, parentPaths: string[], diff --git a/src/lib/openapi/schemas.ts b/src/lib/openapi/schemas.ts index c78fd3b7..77b53858 100644 --- a/src/lib/openapi/schemas.ts +++ b/src/lib/openapi/schemas.ts @@ -81,6 +81,7 @@ export const OpenapiOperationSchema = z.object({ 'x-undocumented': z.string().default(''), 'x-deprecated': z.string().default(''), 'x-draft': z.string().default(''), + 'x-action-attempt-type': z.string().optional(), }) export const EnumValueSchema = z.object({ diff --git a/test/fixtures/types/openapi.ts b/test/fixtures/types/openapi.ts index 23692b30..bff3c545 100644 --- a/test/fixtures/types/openapi.ts +++ b/test/fixtures/types/openapi.ts @@ -318,6 +318,38 @@ export default { 'x-title': 'Get a foo', }, }, + '/foos/create': { + post: { + operationId: 'foosCreatePost', + responses: { + 200: { + content: { + 'application/json': { + schema: { + properties: { + ok: { type: 'boolean' }, + action_attempt: { + $ref: '#/components/schemas/action_attempt', + }, + }, + required: ['action_attempt', 'ok'], + type: 'object', + }, + }, + }, + description: 'Create a foo.', + }, + 400: { description: 'Bad Request' }, + 401: { description: 'Unauthorized' }, + }, + security: [], + summary: '/foos/create', + tags: ['/foos'], + 'x-response-key': 'action_attempt', + 'x-action-attempt-type': 'CREATE_FOO', + 'x-title': 'Create a foo', + }, + }, '/foos/list': { get: { operationId: 'foosListGet', diff --git a/test/snapshots/blueprint.test.ts.md b/test/snapshots/blueprint.test.ts.md index 48f39596..3b629669 100644 --- a/test/snapshots/blueprint.test.ts.md +++ b/test/snapshots/blueprint.test.ts.md @@ -601,6 +601,36 @@ Generated by [AVA](https://avajs.dev). undocumentedMessage: '', workspaceScope: 'required', }, + { + authMethods: [], + codeSamples: [], + deprecationMessage: '', + description: '', + draftMessage: '', + isDeprecated: false, + isDraft: false, + isUndocumented: false, + name: 'create', + path: '/foos/create', + request: { + methods: [ + 'POST', + ], + parameters: [], + preferredMethod: 'POST', + semanticMethod: 'POST', + }, + response: { + actionAttemptType: 'CREATE_FOO', + description: 'Create a foo.', + resourceType: 'action_attempt', + responseKey: 'action_attempt', + responseType: 'resource', + }, + title: 'Create a foo', + undocumentedMessage: '', + workspaceScope: 'none', + }, { authMethods: [ 'api_key', @@ -1727,6 +1757,36 @@ Generated by [AVA](https://avajs.dev). undocumentedMessage: '', workspaceScope: 'required', }, + { + authMethods: [], + codeSamples: [], + deprecationMessage: '', + description: '', + draftMessage: '', + isDeprecated: false, + isDraft: false, + isUndocumented: false, + name: 'create', + path: '/foos/create', + request: { + methods: [ + 'POST', + ], + parameters: [], + preferredMethod: 'POST', + semanticMethod: 'POST', + }, + response: { + actionAttemptType: 'CREATE_FOO', + description: 'Create a foo.', + resourceType: 'action_attempt', + responseKey: 'action_attempt', + responseType: 'resource', + }, + title: 'Create a foo', + undocumentedMessage: '', + workspaceScope: 'none', + }, { authMethods: [ 'api_key', diff --git a/test/snapshots/blueprint.test.ts.snap b/test/snapshots/blueprint.test.ts.snap index bc70860f..b8624b38 100644 Binary files a/test/snapshots/blueprint.test.ts.snap and b/test/snapshots/blueprint.test.ts.snap differ diff --git a/test/snapshots/seam-blueprint.test.ts.md b/test/snapshots/seam-blueprint.test.ts.md index c8c24f74..9e1d40e8 100644 --- a/test/snapshots/seam-blueprint.test.ts.md +++ b/test/snapshots/seam-blueprint.test.ts.md @@ -18871,6 +18871,7 @@ Generated by [AVA](https://avajs.dev). semanticMethod: 'POST', }, response: { + actionAttemptType: 'CREATE_ACCESS_CODE', description: 'OK', resourceType: 'access_code', responseKey: 'access_code', @@ -22073,6 +22074,7 @@ Generated by [AVA](https://avajs.dev). semanticMethod: 'POST', }, response: { + actionAttemptType: 'ENCODE_CREDENTIAL', description: 'OK', resourceType: 'action_attempt', responseKey: 'action_attempt', @@ -22151,6 +22153,7 @@ Generated by [AVA](https://avajs.dev). semanticMethod: 'POST', }, response: { + actionAttemptType: 'SCAN_CREDENTIAL', description: 'OK', resourceType: 'action_attempt', responseKey: 'action_attempt', @@ -28454,6 +28457,7 @@ Generated by [AVA](https://avajs.dev). semanticMethod: 'POST', }, response: { + actionAttemptType: 'LOCK_DOOR', description: 'OK', resourceType: 'action_attempt', responseKey: 'action_attempt', @@ -28514,6 +28518,7 @@ Generated by [AVA](https://avajs.dev). semanticMethod: 'POST', }, response: { + actionAttemptType: 'UNLOCK_DOOR', description: 'OK', resourceType: 'action_attempt', responseKey: 'action_attempt', @@ -29301,6 +29306,7 @@ Generated by [AVA](https://avajs.dev). semanticMethod: 'POST', }, response: { + actionAttemptType: 'CREATE_NOISE_THRESHOLD', description: 'OK', resourceType: 'noise_threshold', responseKey: 'noise_threshold', @@ -30106,6 +30112,7 @@ Generated by [AVA](https://avajs.dev). semanticMethod: 'POST', }, response: { + actionAttemptType: 'ACTIVATE_CLIMATE_PRESET', description: 'OK', resourceType: 'action_attempt', responseKey: 'action_attempt', @@ -30192,6 +30199,7 @@ Generated by [AVA](https://avajs.dev). semanticMethod: 'POST', }, response: { + actionAttemptType: 'SET_HVAC_MODE', description: 'OK', resourceType: 'action_attempt', responseKey: 'action_attempt', @@ -30630,6 +30638,7 @@ Generated by [AVA](https://avajs.dev). semanticMethod: 'POST', }, response: { + actionAttemptType: 'SET_HVAC_MODE', description: 'OK', resourceType: 'action_attempt', responseKey: 'action_attempt', @@ -30742,6 +30751,7 @@ Generated by [AVA](https://avajs.dev). semanticMethod: 'POST', }, response: { + actionAttemptType: 'SET_HVAC_MODE', description: 'OK', resourceType: 'action_attempt', responseKey: 'action_attempt', @@ -31354,6 +31364,7 @@ Generated by [AVA](https://avajs.dev). semanticMethod: 'POST', }, response: { + actionAttemptType: 'SET_HVAC_MODE', description: 'OK', resourceType: 'action_attempt', responseKey: 'action_attempt', @@ -31561,6 +31572,7 @@ Generated by [AVA](https://avajs.dev). semanticMethod: 'POST', }, response: { + actionAttemptType: 'SET_FAN_MODE', description: 'OK', resourceType: 'action_attempt', responseKey: 'action_attempt', @@ -31594,6 +31606,7 @@ Generated by [AVA](https://avajs.dev). semanticMethod: 'POST', }, response: { + actionAttemptType: 'SET_HVAC_MODE', description: 'OK', resourceType: 'action_attempt', responseKey: 'action_attempt', @@ -33890,6 +33903,7 @@ Generated by [AVA](https://avajs.dev). semanticMethod: 'POST', }, response: { + actionAttemptType: 'RESET_SANDBOX_WORKSPACE', description: 'OK', resourceType: 'action_attempt', responseKey: 'action_attempt', diff --git a/test/snapshots/seam-blueprint.test.ts.snap b/test/snapshots/seam-blueprint.test.ts.snap index 4da068d1..8ddadda2 100644 Binary files a/test/snapshots/seam-blueprint.test.ts.snap and b/test/snapshots/seam-blueprint.test.ts.snap differ