Skip to content

Commit

Permalink
feat: Add formatCode option (#69)
Browse files Browse the repository at this point in the history
* Make blueprint async

* Make createBlueprint and createCodeSample async

* Add formatCode support
  • Loading branch information
razor-x authored Aug 29, 2024
1 parent 4a97ea6 commit c6a0117
Show file tree
Hide file tree
Showing 11 changed files with 638 additions and 100 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ import { createBlueprint, TypesModuleSchema } from '@seamapi/blueprint'
import * as types from '@seamapi/types/connect'

const typesModule = TypesModuleSchema.parse(types)
const blueprint = createBlueprint(typesModule)
const blueprint = await createBlueprint(typesModule)
console.log(JSON.stringify(blueprint)
```
Expand Down
3 changes: 2 additions & 1 deletion examples/blueprint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,6 @@ export const builder: Builder = {

export const handler: Handler<Options> = async ({ moduleName, logger }) => {
const types = TypesModuleSchema.parse(await import(moduleName))
logger.info({ data: createBlueprint(types) }, 'blueprint')
const blueprint = await createBlueprint(types)
logger.info({ blueprint }, 'blueprint')
}
110 changes: 66 additions & 44 deletions src/lib/blueprint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -206,7 +209,7 @@ interface IdProperty extends BaseProperty {

export type Method = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'

interface Context {
interface Context extends Required<BlueprintOptions> {
codeSampleDefinitions: CodeSampleDefinition[]
}

Expand All @@ -220,7 +223,14 @@ export type TypesModuleInput = z.input<typeof TypesModuleSchema>

export type TypesModule = z.output<typeof TypesModuleSchema>

export const createBlueprint = (typesModule: TypesModuleInput): Blueprint => {
export interface BlueprintOptions {
formatCode?: (content: string, syntax: CodeSampleSyntax) => Promise<string>
}

export const createBlueprint = async (
typesModule: TypesModuleInput,
{ formatCode = async (content) => content }: BlueprintOptions = {},
): Promise<Blueprint> => {
const { codeSampleDefinitions } = TypesModuleSchema.parse(typesModule)

// TODO: Move openapi to TypesModuleSchema
Expand All @@ -232,11 +242,12 @@ export const createBlueprint = (typesModule: TypesModuleInput): Blueprint => {

const context = {
codeSampleDefinitions,
formatCode,
}

return {
title: openapi.info.title,
routes: createRoutes(openapi.paths, isFakeData, targetPath, context),
routes: await createRoutes(openapi.paths, isFakeData, targetPath, context),
resources: createResources(
openapi.components.schemas,
isFakeData,
Expand All @@ -245,77 +256,82 @@ export const createBlueprint = (typesModule: TypesModuleInput): Blueprint => {
}
}

const createRoutes = (
const createRoutes = async (
paths: OpenapiPaths,
isFakeData: boolean,
targetPath: string,
context: Context,
): Route[] => {
): Promise<Route[]> => {
const routeMap = new Map<string, Route>()

Object.entries(paths)
.filter(([path]) => isFakeData || path.startsWith(targetPath))
.forEach(([path, pathItem]) => {
const route = createRoute(path, pathItem, context)
const pathEntries = Object.entries(paths).filter(
([path]) => isFakeData || path.startsWith(targetPath),
)

const existingRoute = routeMap.get(route.path)
if (existingRoute != null) {
existingRoute.endpoints.push(...route.endpoints)
} else {
routeMap.set(route.path, route)
}
})
for (const [path, pathItem] of pathEntries) {
const route = await createRoute(path, pathItem, context)

const existingRoute = routeMap.get(route.path)
if (existingRoute != null) {
existingRoute.endpoints.push(...route.endpoints)
continue
}

routeMap.set(route.path, route)
}

return Array.from(routeMap.values())
}

const createRoute = (
const createRoute = async (
path: string,
pathItem: OpenapiPathItem,
context: Context,
): Route => {
): Promise<Route> => {
const pathParts = path.split('/')
const routePath = `/${pathParts.slice(1, -1).join('/')}`

return {
path: routePath,
namespace: { path: `/${pathParts[1]}` },
endpoints: createEndpoints(path, pathItem, context),
endpoints: await createEndpoints(path, pathItem, context),
subroutes: [],
}
}

const createEndpoints = (
const createEndpoints = async (
path: string,
pathItem: OpenapiPathItem,
context: Context,
): Endpoint[] => {
): Promise<Endpoint[]> => {
const validMethods: Method[] = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH']

return Object.entries(pathItem)
.filter(
([method, operation]) =>
validMethods.includes(method.toUpperCase() as Method) &&
typeof operation === 'object' &&
operation !== null,
)
.map(([method, operation]) => {
const uppercaseMethod = method.toUpperCase() as Method
return createEndpoint(
[uppercaseMethod],
operation as OpenapiOperation,
path,
context,
return await Promise.all(
Object.entries(pathItem)
.filter(
([method, operation]) =>
validMethods.includes(method.toUpperCase() as Method) &&
typeof operation === 'object' &&
operation !== null,
)
})
.map(async ([method, operation]) => {
const uppercaseMethod = method.toUpperCase() as Method
return await createEndpoint(
[uppercaseMethod],
operation as OpenapiOperation,
path,
context,
)
}),
)
}

const createEndpoint = (
const createEndpoint = async (
methods: Method[],
operation: OpenapiOperation,
path: string,
context: Context,
): Endpoint => {
): Promise<Endpoint> => {
const pathParts = path.split('/')
const endpointPath = `/${pathParts.slice(1).join('/')}`

Expand Down Expand Up @@ -346,11 +362,17 @@ const createEndpoint = (

return {
...endpoint,
codeSamples: context.codeSampleDefinitions
.filter(({ request }) => request.path === endpointPath)
.map((codeSampleDefinition) =>
createCodeSample(codeSampleDefinition, { endpoint }),
),
codeSamples: await Promise.all(
context.codeSampleDefinitions
.filter(({ request }) => request.path === endpointPath)
.map(
async (codeSampleDefinition) =>
await createCodeSample(codeSampleDefinition, {
endpoint,
formatCode: context.formatCode,
}),
),
),
}
}

Expand Down
43 changes: 43 additions & 0 deletions src/lib/code-sample/format.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import type { Code, Context } from './schema.js'

type CodeEntries = Entries<Code>
type CodeEntry = NonNullable<CodeEntries[number]>

export const formatCodeRecords = async (
code: Code,
context: Context,
): Promise<Code> => {
const entries = Object.entries(code) as unknown as CodeEntries
const formattedEntries = await Promise.all(
entries.map(async (entry): Promise<CodeEntry> => {
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<CodeEntry> => {
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<T> = Array<
{
[K in keyof T]: [K, T[K]]
}[keyof T]
>
1 change: 1 addition & 0 deletions src/lib/code-sample/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@ export {
type CodeSample,
type CodeSampleDefinitionInput,
CodeSampleDefinitionSchema,
type CodeSampleSyntax,
createCodeSample,
} from './schema.js'
Loading

0 comments on commit c6a0117

Please sign in to comment.