diff --git a/src/lib/blueprint.ts b/src/lib/blueprint.ts index 021effe..86bfbd2 100644 --- a/src/lib/blueprint.ts +++ b/src/lib/blueprint.ts @@ -5,7 +5,10 @@ import { CodeSampleDefinitionSchema, createCodeSample, } from './code-sample/index.js' -import type { CodeSampleDefinition } from './code-sample/schema.js' +import type { + CodeSampleDefinition, + CodeSampleSyntax, +} from './code-sample/schema.js' import type { Openapi, OpenapiOperation, @@ -221,7 +224,7 @@ export type TypesModuleInput = z.input export type TypesModule = z.output export interface BlueprintOptions { - formatCode?: (content: string) => Promise + formatCode?: (content: string, syntax: CodeSampleSyntax) => Promise } export const createBlueprint = async ( @@ -362,11 +365,12 @@ const createEndpoint = async ( codeSamples: await Promise.all( context.codeSampleDefinitions .filter(({ request }) => request.path === endpointPath) - .map(async (codeSampleDefinition) => - await createCodeSample(codeSampleDefinition, { - endpoint, - formatCode: context.formatCode, - }), + .map( + async (codeSampleDefinition) => + await createCodeSample(codeSampleDefinition, { + endpoint, + formatCode: context.formatCode, + }), ), ), } diff --git a/src/lib/code-sample/format.ts b/src/lib/code-sample/format.ts new file mode 100644 index 0000000..e7c4653 --- /dev/null +++ b/src/lib/code-sample/format.ts @@ -0,0 +1,43 @@ +import type { Code, Context } from './schema.js' + +type CodeEntries = Entries +type CodeEntry = NonNullable + +export const formatCodeRecords = async ( + code: Code, + context: Context, +): Promise => { + const entries = Object.entries(code) as unknown as CodeEntries + const formattedEntries = await Promise.all( + entries.map(async (entry): Promise => { + if (entry == null) throw new Error('Unexpected null code entry') + return await formatCodeEntry(entry, context) + }), + ) + return Object.fromEntries(formattedEntries) +} + +const formatCodeEntry = async ( + [key, code]: CodeEntry, + { formatCode }: Context, +): Promise => { + if (code == null) throw new Error(`Unexpected null in code object for ${key}`) + const [request, response] = await Promise.all([ + await formatCode(code.request, code.request_syntax), + await formatCode(code.response, code.response_syntax), + ]) + return [ + key, + { + ...code, + request, + response, + }, + ] +} + +type Entries = Array< + { + [K in keyof T]: [K, T[K]] + }[keyof T] +> diff --git a/src/lib/code-sample/index.ts b/src/lib/code-sample/index.ts index d094987..6768c78 100644 --- a/src/lib/code-sample/index.ts +++ b/src/lib/code-sample/index.ts @@ -2,5 +2,6 @@ export { type CodeSample, type CodeSampleDefinitionInput, CodeSampleDefinitionSchema, + type CodeSampleSyntax, createCodeSample, } from './schema.js' diff --git a/src/lib/code-sample/schema.ts b/src/lib/code-sample/schema.ts index 7574dde..937e5b3 100644 --- a/src/lib/code-sample/schema.ts +++ b/src/lib/code-sample/schema.ts @@ -7,6 +7,7 @@ import { } from 'lib/code-sample/seam-cli.js' import { JsonSchema } from 'lib/json.js' +import { formatCodeRecords } from './format.js' import { createJavascriptRequest, createJavascriptResponse, @@ -39,72 +40,85 @@ export type CodeSampleDefinitionInput = z.input< export type CodeSampleDefinition = z.output -const syntax = z.enum(['javascript', 'json', 'python', 'php', 'ruby', 'bash']) +const CodeSampleSyntaxSchema = z.enum([ + 'javascript', + 'json', + 'python', + 'php', + 'ruby', + 'bash', +]) -export type Syntax = z.infer +export type CodeSampleSyntax = z.infer + +const CodeSchema = z.record( + z.enum(['javascript', 'python', 'php', 'ruby', 'seam_cli']), + z.object({ + title: z.string().min(1), + request: z.string(), + response: z.string(), + request_syntax: CodeSampleSyntaxSchema, + response_syntax: CodeSampleSyntaxSchema, + }), +) + +export type Code = z.infer const CodeSampleSchema = CodeSampleDefinitionSchema.extend({ - code: z.record( - z.enum(['javascript', 'python', 'php', 'ruby', 'seam_cli']), - z.object({ - title: z.string().min(1), - request: z.string(), - response: z.string(), - request_syntax: syntax, - response_syntax: syntax, - }), - ), + code: CodeSchema, }) export type CodeSample = z.output export interface Context { endpoint: Omit - formatCode: (content: string, syntax: Syntax) => Promise + formatCode: (content: string, syntax: CodeSampleSyntax) => Promise } export const createCodeSample = async ( codeSampleDefinition: CodeSampleDefinition, context: Context, ): Promise => { + const code: Code = { + javascript: { + title: 'JavaScript', + request: createJavascriptRequest(codeSampleDefinition, context), + response: createJavascriptResponse(codeSampleDefinition, context), + request_syntax: 'javascript', + response_syntax: 'javascript', + }, + python: { + title: 'Python', + request: createPythonRequest(codeSampleDefinition, context), + response: createPythonResponse(codeSampleDefinition, context), + request_syntax: 'python', + response_syntax: 'python', + }, + ruby: { + title: 'Ruby', + request: createRubyRequest(codeSampleDefinition, context), + response: createRubyResponse(codeSampleDefinition, context), + request_syntax: 'ruby', + response_syntax: 'ruby', + }, + php: { + title: 'PHP', + request: createPhpRequest(codeSampleDefinition, context), + response: createPhpResponse(codeSampleDefinition, context), + request_syntax: 'php', + response_syntax: 'json', + }, + seam_cli: { + title: 'Seam CLI', + request: createSeamCliRequest(codeSampleDefinition, context), + response: createSeamCliResponse(codeSampleDefinition, context), + request_syntax: 'bash', + response_syntax: 'json', + }, + } + return { ...codeSampleDefinition, - code: { - javascript: { - title: 'JavaScript', - request: createJavascriptRequest(codeSampleDefinition, context), - response: createJavascriptResponse(codeSampleDefinition, context), - request_syntax: 'javascript', - response_syntax: 'javascript', - }, - python: { - title: 'Python', - request: createPythonRequest(codeSampleDefinition, context), - response: createPythonResponse(codeSampleDefinition, context), - request_syntax: 'python', - response_syntax: 'python', - }, - ruby: { - title: 'Ruby', - request: createRubyRequest(codeSampleDefinition, context), - response: createRubyResponse(codeSampleDefinition, context), - request_syntax: 'ruby', - response_syntax: 'ruby', - }, - php: { - title: 'PHP', - request: createPhpRequest(codeSampleDefinition, context), - response: createPhpResponse(codeSampleDefinition, context), - request_syntax: 'php', - response_syntax: 'json', - }, - seam_cli: { - title: 'Seam CLI', - request: createSeamCliRequest(codeSampleDefinition, context), - response: createSeamCliResponse(codeSampleDefinition, context), - request_syntax: 'bash', - response_syntax: 'json', - }, - }, + code: await formatCodeRecords(code, context), } } diff --git a/src/lib/index.ts b/src/lib/index.ts index 39b86b1..812a7a9 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -18,4 +18,5 @@ export { type CodeSample, type CodeSampleDefinitionInput, CodeSampleDefinitionSchema, + type CodeSampleSyntax, } from './code-sample/index.js' diff --git a/test/blueprint.test.ts b/test/blueprint.test.ts index ea137ef..21ff18e 100644 --- a/test/blueprint.test.ts +++ b/test/blueprint.test.ts @@ -9,3 +9,11 @@ test('createBlueprint', async (t) => { const blueprint = await createBlueprint(typesModule) t.snapshot(blueprint, 'blueprint') }) + +test('createBlueprint: with formatCode', async (t) => { + const typesModule = TypesModuleSchema.parse(types) + const blueprint = await createBlueprint(typesModule, { + formatCode: async (content, syntax) => [`// ${syntax}`, content].join('\n'), + }) + t.snapshot(blueprint, 'blueprint') +}) diff --git a/test/snapshots/blueprint.test.ts.md b/test/snapshots/blueprint.test.ts.md index 04d368f..f783d05 100644 --- a/test/snapshots/blueprint.test.ts.md +++ b/test/snapshots/blueprint.test.ts.md @@ -407,3 +407,447 @@ Generated by [AVA](https://avajs.dev). ], title: 'Foo', } + +## createBlueprint: with formatCode + +> blueprint + + { + resources: { + foo: { + description: 'A foo resource.', + properties: [ + { + deprecationMessage: '', + description: 'Foo id', + format: 'id', + isDeprecated: false, + isUndocumented: false, + jsonType: 'string', + name: 'foo_id', + }, + { + deprecationMessage: '', + description: 'Foo name', + format: 'string', + isDeprecated: false, + isUndocumented: false, + jsonType: 'string', + name: 'name', + }, + { + deprecationMessage: 'This prop will be removed in the next version', + description: 'This prop is deprecated', + format: 'string', + isDeprecated: true, + isUndocumented: false, + jsonType: 'string', + name: 'deprecated_prop', + }, + { + deprecationMessage: '', + description: 'This prop is undocumented', + format: 'string', + isDeprecated: false, + isUndocumented: true, + jsonType: 'string', + name: 'undocumented_prop', + }, + { + deprecationMessage: '', + description: 'This prop is nullable', + format: 'string', + isDeprecated: false, + isUndocumented: false, + jsonType: 'string', + name: 'nullable_property', + }, + ], + resourceType: 'foo', + }, + }, + routes: [ + { + endpoints: [ + { + codeSamples: [ + { + code: { + javascript: { + request: `// javascript␊ + await seam.foos.get({"foo_id":"8d7e0b3a-b889-49a7-9164-4b71a0506a33"})`, + request_syntax: 'javascript', + response: `// javascript␊ + {"foo_id":"8d7e0b3a-b889-49a7-9164-4b71a0506a33","name":"Best foo","nullable_property":null}`, + response_syntax: 'javascript', + title: 'JavaScript', + }, + php: { + request: `// php␊ + $seam->foos->get(foo_id:"8d7e0b3a-b889-49a7-9164-4b71a0506a33")`, + request_syntax: 'php', + response: `// json␊ + {"foo_id":"8d7e0b3a-b889-49a7-9164-4b71a0506a33","name":"Best foo","nullable_property":null}`, + response_syntax: 'json', + title: 'PHP', + }, + python: { + request: `// python␊ + seam.foos.get(foo_id="8d7e0b3a-b889-49a7-9164-4b71a0506a33")`, + request_syntax: 'python', + response: `// python␊ + Foo(foo_id="8d7e0b3a-b889-49a7-9164-4b71a0506a33", name="Best foo", nullable_property=None)`, + response_syntax: 'python', + title: 'Python', + }, + ruby: { + request: `// ruby␊ + seam.foos.get(foo_id: "8d7e0b3a-b889-49a7-9164-4b71a0506a33")`, + request_syntax: 'ruby', + response: `// ruby␊ + `, + response_syntax: 'ruby', + title: 'Ruby', + }, + seam_cli: { + request: `// bash␊ + seam foos get --foo_id "8d7e0b3a-b889-49a7-9164-4b71a0506a33"`, + request_syntax: 'bash', + response: `// json␊ + {"foo_id":"8d7e0b3a-b889-49a7-9164-4b71a0506a33","name":"Best foo","nullable_property":null}`, + response_syntax: 'json', + title: 'Seam CLI', + }, + }, + description: 'This is the way to get a foo', + request: { + parameters: { + foo_id: '8d7e0b3a-b889-49a7-9164-4b71a0506a33', + }, + path: '/foos/get', + }, + response: { + body: { + foo: { + foo_id: '8d7e0b3a-b889-49a7-9164-4b71a0506a33', + name: 'Best foo', + nullable_property: null, + }, + }, + }, + title: 'Get a foo by ID', + }, + ], + deprecationMessage: '', + description: '', + isDeprecated: false, + isUndocumented: false, + path: '/foos/get', + request: { + methods: [ + 'GET', + ], + parameters: [], + preferredMethod: 'GET', + semanticMethod: 'GET', + }, + response: { + description: 'Get a foo by ID.', + resourceType: 'foo', + responseKey: 'foo', + responseType: 'resource', + }, + title: 'Get a foo', + }, + { + codeSamples: [ + { + code: { + javascript: { + request: `// javascript␊ + await seam.foos.get({"foo_id":"8d7e0b3a-b889-49a7-9164-4b71a0506a33"})`, + request_syntax: 'javascript', + response: `// javascript␊ + {"foo_id":"8d7e0b3a-b889-49a7-9164-4b71a0506a33","name":"Best foo","nullable_property":null}`, + response_syntax: 'javascript', + title: 'JavaScript', + }, + php: { + request: `// php␊ + $seam->foos->get(foo_id:"8d7e0b3a-b889-49a7-9164-4b71a0506a33")`, + request_syntax: 'php', + response: `// json␊ + {"foo_id":"8d7e0b3a-b889-49a7-9164-4b71a0506a33","name":"Best foo","nullable_property":null}`, + response_syntax: 'json', + title: 'PHP', + }, + python: { + request: `// python␊ + seam.foos.get(foo_id="8d7e0b3a-b889-49a7-9164-4b71a0506a33")`, + request_syntax: 'python', + response: `// python␊ + Foo(foo_id="8d7e0b3a-b889-49a7-9164-4b71a0506a33", name="Best foo", nullable_property=None)`, + response_syntax: 'python', + title: 'Python', + }, + ruby: { + request: `// ruby␊ + seam.foos.get(foo_id: "8d7e0b3a-b889-49a7-9164-4b71a0506a33")`, + request_syntax: 'ruby', + response: `// ruby␊ + `, + response_syntax: 'ruby', + title: 'Ruby', + }, + seam_cli: { + request: `// bash␊ + seam foos get --foo_id "8d7e0b3a-b889-49a7-9164-4b71a0506a33"`, + request_syntax: 'bash', + response: `// json␊ + {"foo_id":"8d7e0b3a-b889-49a7-9164-4b71a0506a33","name":"Best foo","nullable_property":null}`, + response_syntax: 'json', + title: 'Seam CLI', + }, + }, + description: 'This is the way to get a foo', + request: { + parameters: { + foo_id: '8d7e0b3a-b889-49a7-9164-4b71a0506a33', + }, + path: '/foos/get', + }, + response: { + body: { + foo: { + foo_id: '8d7e0b3a-b889-49a7-9164-4b71a0506a33', + name: 'Best foo', + nullable_property: null, + }, + }, + }, + title: 'Get a foo by ID', + }, + ], + deprecationMessage: '', + description: '', + isDeprecated: false, + isUndocumented: false, + path: '/foos/get', + request: { + methods: [ + 'POST', + ], + parameters: [], + preferredMethod: 'POST', + semanticMethod: 'POST', + }, + response: { + description: 'Get a foo by ID.', + resourceType: 'foo', + responseKey: 'foo', + responseType: 'resource', + }, + title: 'Get a foo', + }, + { + codeSamples: [ + { + code: { + javascript: { + request: `// javascript␊ + await seam.foos.list()`, + request_syntax: 'javascript', + response: `// javascript␊ + [{"foo_id":"8d7e0b3a-b889-49a7-9164-4b71a0506a33","name":"Best foo","nullable_property":null}]`, + response_syntax: 'javascript', + title: 'JavaScript', + }, + php: { + request: `// php␊ + $seam->foos->list()`, + request_syntax: 'php', + response: `// json␊ + [{"foo_id":"8d7e0b3a-b889-49a7-9164-4b71a0506a33","name":"Best foo","nullable_property":null}]`, + response_syntax: 'json', + title: 'PHP', + }, + python: { + request: `// python␊ + seam.foos.list()`, + request_syntax: 'python', + response: `// python␊ + [Foo(foo_id="8d7e0b3a-b889-49a7-9164-4b71a0506a33", name="Best foo", nullable_property=None)]`, + response_syntax: 'python', + title: 'Python', + }, + ruby: { + request: `// ruby␊ + seam.foos.list()`, + request_syntax: 'ruby', + response: `// ruby␊ + []`, + response_syntax: 'ruby', + title: 'Ruby', + }, + seam_cli: { + request: `// bash␊ + seam foos list `, + request_syntax: 'bash', + response: `// json␊ + [{"foo_id":"8d7e0b3a-b889-49a7-9164-4b71a0506a33","name":"Best foo","nullable_property":null}]`, + response_syntax: 'json', + title: 'Seam CLI', + }, + }, + description: 'This is the way to list foos', + request: { + parameters: {}, + path: '/foos/list', + }, + response: { + body: { + foos: [ + { + foo_id: '8d7e0b3a-b889-49a7-9164-4b71a0506a33', + name: 'Best foo', + nullable_property: null, + }, + ], + }, + }, + title: 'List foos', + }, + ], + deprecationMessage: '', + description: '', + isDeprecated: false, + isUndocumented: false, + path: '/foos/list', + request: { + methods: [ + 'GET', + ], + parameters: [], + preferredMethod: 'GET', + semanticMethod: 'GET', + }, + response: { + description: 'List all foos.', + resourceType: 'foo', + responseKey: 'foos', + responseType: 'resource_list', + }, + title: 'List foos', + }, + { + codeSamples: [ + { + code: { + javascript: { + request: `// javascript␊ + await seam.foos.list()`, + request_syntax: 'javascript', + response: `// javascript␊ + [{"foo_id":"8d7e0b3a-b889-49a7-9164-4b71a0506a33","name":"Best foo","nullable_property":null}]`, + response_syntax: 'javascript', + title: 'JavaScript', + }, + php: { + request: `// php␊ + $seam->foos->list()`, + request_syntax: 'php', + response: `// json␊ + [{"foo_id":"8d7e0b3a-b889-49a7-9164-4b71a0506a33","name":"Best foo","nullable_property":null}]`, + response_syntax: 'json', + title: 'PHP', + }, + python: { + request: `// python␊ + seam.foos.list()`, + request_syntax: 'python', + response: `// python␊ + [Foo(foo_id="8d7e0b3a-b889-49a7-9164-4b71a0506a33", name="Best foo", nullable_property=None)]`, + response_syntax: 'python', + title: 'Python', + }, + ruby: { + request: `// ruby␊ + seam.foos.list()`, + request_syntax: 'ruby', + response: `// ruby␊ + []`, + response_syntax: 'ruby', + title: 'Ruby', + }, + seam_cli: { + request: `// bash␊ + seam foos list `, + request_syntax: 'bash', + response: `// json␊ + [{"foo_id":"8d7e0b3a-b889-49a7-9164-4b71a0506a33","name":"Best foo","nullable_property":null}]`, + response_syntax: 'json', + title: 'Seam CLI', + }, + }, + description: 'This is the way to list foos', + request: { + parameters: {}, + path: '/foos/list', + }, + response: { + body: { + foos: [ + { + foo_id: '8d7e0b3a-b889-49a7-9164-4b71a0506a33', + name: 'Best foo', + nullable_property: null, + }, + ], + }, + }, + title: 'List foos', + }, + ], + deprecationMessage: '', + description: '', + isDeprecated: false, + isUndocumented: false, + path: '/foos/list', + request: { + methods: [ + 'POST', + ], + parameters: [], + preferredMethod: 'POST', + semanticMethod: 'POST', + }, + response: { + description: 'List all foos.', + resourceType: 'foo', + responseKey: 'foos', + responseType: 'resource_list', + }, + title: 'List foos', + }, + ], + namespace: { + path: '/foos', + }, + path: '/foos', + subroutes: [], + }, + ], + title: 'Foo', + }