Skip to content

Commit

Permalink
feat: Add routePath to Resource and blueprint.events (#141)
Browse files Browse the repository at this point in the history
Co-authored-by: Seam Bot <[email protected]>
  • Loading branch information
andrii-balitskyi and seambot authored Dec 20, 2024
1 parent 87880b2 commit 8e09da6
Show file tree
Hide file tree
Showing 12 changed files with 10,293 additions and 2,421 deletions.
2 changes: 1 addition & 1 deletion src/lib/blueprint.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
type Method,
type OpenapiAuthMethod,
} from 'lib/blueprint.js'
import type { OpenapiOperation, OpenapiSchema } from 'lib/openapi.js'
import type { OpenapiOperation, OpenapiSchema } from 'lib/openapi/types.js'

test('createProperties: assigns appropriate default values', (t) => {
const minimalProperties = {
Expand Down
130 changes: 101 additions & 29 deletions src/lib/blueprint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,23 +9,27 @@ import type {
CodeSampleDefinition,
CodeSampleSyntax,
} from './code-sample/schema.js'
import { findCommonOpenapiSchemaProperties } from './openapi/find-common-openapi-schema-properties.js'
import {
type AuthMethodSchema,
EventResourceSchema,
OpenapiOperationSchema,
PropertySchema,
ResourceSchema,
} from './openapi/schemas.js'
import type {
Openapi,
OpenapiOperation,
OpenapiPathItem,
OpenapiPaths,
OpenapiSchema,
} from './openapi.js'
import {
type AuthMethodSchema,
OpenapiOperationSchema,
PropertySchema,
} from './openapi-schema.js'
} from './openapi/types.js'

export interface Blueprint {
title: string
routes: Route[]
resources: Record<string, Resource>
events: EventResource[]
}

export interface Route {
Expand All @@ -43,6 +47,7 @@ export interface Resource {
resourceType: string
properties: Property[]
description: string
routePath: string
isDeprecated: boolean
deprecationMessage: string
isUndocumented: boolean
Expand All @@ -51,6 +56,12 @@ export interface Resource {
draftMessage: string
}

interface EventResource extends Resource {
resourceType: 'event'
eventType: string
targetResourceType: string | null
}

export interface Namespace {
path: string
isDeprecated: boolean
Expand Down Expand Up @@ -277,6 +288,43 @@ export interface BlueprintOptions {
formatCode?: (content: string, syntax: CodeSampleSyntax) => Promise<string>
}

const createEvents = (
schemas: Openapi['components']['schemas'],
resources: Record<string, Resource>,
): EventResource[] => {
const eventSchema = schemas['event']
if (
eventSchema == null ||
typeof eventSchema !== 'object' ||
!('oneOf' in eventSchema) ||
!Array.isArray(eventSchema.oneOf)
) {
return []
}

return eventSchema.oneOf
.map((schema) => {
if (
typeof schema !== 'object' ||
schema.properties?.event_type?.enum?.[0] == null
) {
return null
}

const eventType = schema.properties.event_type.enum[0]
const targetResourceType = Object.keys(resources).find((resourceName) =>
eventType.split('.').includes(resourceName),
)

return {
...createResource('event', schema as OpenapiSchema),
eventType,
targetResourceType: targetResourceType ?? null,
}
})
.filter((event): event is EventResource => event !== null)
}

export const createBlueprint = async (
typesModule: TypesModuleInput,
{ formatCode = async (content) => content }: BlueprintOptions = {},
Expand All @@ -291,10 +339,13 @@ export const createBlueprint = async (
formatCode,
}

const resources = createResources(openapi.components.schemas)

return {
title: openapi.info.title,
routes: await createRoutes(openapi.paths, context),
resources: createResources(openapi.components.schemas),
resources,
events: createEvents(openapi.components.schemas, resources),
}
}

Expand Down Expand Up @@ -752,39 +803,60 @@ const createParameter = (
}
}

const createResources = (
export const createResources = (
schemas: Openapi['components']['schemas'],
): Record<string, Resource> => {
return Object.entries(schemas).reduce<Record<string, Resource>>(
(acc, [schemaName, schema]) => {
if (
typeof schema === 'object' &&
schema !== null &&
'properties' in schema &&
typeof schema.properties === 'object' &&
schema.properties !== null
) {
(resources, [schemaName, schema]) => {
const { success: isValidEventSchema, data: parsedEvent } =
EventResourceSchema.safeParse(schema)
if (isValidEventSchema) {
const commonProperties = findCommonOpenapiSchemaProperties(
parsedEvent.oneOf,
)
const eventSchema: OpenapiSchema = {
properties: commonProperties,
type: 'object',
}
return {
...resources,
[schemaName]: createResource(schemaName, eventSchema),
}
}

const { success: isValidResourceSchema } =
ResourceSchema.safeParse(schema)
if (isValidResourceSchema) {
return {
...acc,
[schemaName]: {
resourceType: schemaName,
properties: createProperties(schema.properties, [schemaName]),
description: schema.description ?? '',
isDeprecated: schema.deprecated ?? false,
deprecationMessage: schema['x-deprecated'] ?? '',
isUndocumented: (schema['x-undocumented'] ?? '').length > 0,
undocumentedMessage: schema['x-undocumented'] ?? '',
isDraft: (schema['x-draft'] ?? '').length > 0,
draftMessage: schema['x-draft'] ?? '',
},
...resources,
[schemaName]: createResource(schemaName, schema),
}
}
return acc

return resources
},
{},
)
}

const createResource = (
schemaName: string,
schema: OpenapiSchema,
): Resource => {
return {
resourceType: schemaName,
properties: createProperties(schema.properties ?? {}, [schemaName]),
description: schema.description ?? '',
isDeprecated: schema.deprecated ?? false,
routePath: schema['x-route-path'] ?? '',
deprecationMessage: schema['x-deprecated'] ?? '',
isUndocumented: (schema['x-undocumented'] ?? '').length > 0,
undocumentedMessage: schema['x-undocumented'] ?? '',
isDraft: (schema['x-draft'] ?? '').length > 0,
draftMessage: schema['x-draft'] ?? '',
}
}

const createResponse = (
operation: OpenapiOperation,
path: string,
Expand Down
38 changes: 38 additions & 0 deletions src/lib/openapi/find-common-openapi-schema-properties.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import test from 'ava'

import type { OpenapiSchema } from 'lib/openapi/types.js'

import { findCommonOpenapiSchemaProperties } from './find-common-openapi-schema-properties.js'

test('findCommonOpenapiSchemaProperties: extracts common properties from openapi schemas', (t) => {
const schemas: OpenapiSchema[] = [
{
type: 'object',
properties: {
event_id: { type: 'string', format: 'uuid' },
event_type: { type: 'string' },
created_at: { type: 'string', format: 'date-time' },
foo_id: { type: 'string' },
},
},
{
type: 'object',
properties: {
event_id: { type: 'string', format: 'uuid' },
event_type: { type: 'string' },
created_at: { type: 'string', format: 'date-time' },
bar_id: { type: 'string' },
},
},
]

const commonProps = findCommonOpenapiSchemaProperties(schemas)
const commonKeys = Object.keys(commonProps)

t.is(commonKeys.length, 3)
t.true(
['event_id', 'event_type', 'created_at'].every((key) =>
commonKeys.includes(key),
),
)
})
36 changes: 36 additions & 0 deletions src/lib/openapi/find-common-openapi-schema-properties.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import type { OpenapiSchema } from 'lib/openapi/types.js'

export function findCommonOpenapiSchemaProperties(
schemas: OpenapiSchema[],
): Record<string, OpenapiSchema> {
const firstSchema = schemas[0]
if (schemas.length === 0 || firstSchema?.properties == null) {
return {}
}

return Object.entries(firstSchema.properties).reduce<
Record<string, OpenapiSchema>
>((commonProps, [propKey, propValue]) => {
const isPropInAllSchemas = schemas.every((schema) =>
Object.keys(schema.properties ?? {}).includes(propKey),
)

if (!isPropInAllSchemas) {
return commonProps
}

if ('enum' in propValue) {
const mergedEnumValues = schemas.reduce<string[]>((allEnums, schema) => {
const enumValues = schema.properties?.[propKey]?.enum ?? []
return [...new Set([...allEnums, ...enumValues])]
}, [])

return {
...commonProps,
[propKey]: { ...propValue, enum: mergedEnumValues },
}
}

return { ...commonProps, [propKey]: propValue }
}, {})
}
3 changes: 3 additions & 0 deletions src/lib/openapi/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './find-common-openapi-schema-properties.js'
export * from './schemas.js'
export * from './types.js'
16 changes: 16 additions & 0 deletions src/lib/openapi-schema.ts → src/lib/openapi/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,3 +94,19 @@ export const PropertySchema: z.ZodSchema<any> = z.object({
$ref: z.string().optional(),
format: z.string().optional(),
})

export const ResourceSchema = z.object({
type: z.literal('object'),
properties: z.record(z.string(), PropertySchema),
required: z.array(z.string()).default([]),
description: z.string().default(''),
'x-route-path': z.string().default(''),
'x-undocumented': z.string().default(''),
'x-deprecated': z.string().default(''),
'x-draft': z.string().default(''),
})

export const EventResourceSchema = z.object({
discriminator: z.object({ propertyName: z.string() }),
oneOf: z.array(ResourceSchema),
})
2 changes: 2 additions & 0 deletions src/lib/openapi.ts → src/lib/openapi/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,12 +71,14 @@ export interface OpenapiSchema {
items?: OpenapiSchema
$ref?: string
required?: string[]
enum?: string[]
format?: string
description?: string
deprecated?: boolean
'x-deprecated'?: string
'x-draft'?: string
'x-undocumented'?: string
'x-route-path'?: string
}

export interface OpenapiComponents {
Expand Down
37 changes: 37 additions & 0 deletions test/fixtures/types/openapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ export default {
},
},
required: ['foo_id', 'name'],
'x-route-path': '/foos',
},
plane: {
type: 'object',
Expand All @@ -69,6 +70,7 @@ export default {
},
},
required: ['plane_id', 'name'],
'x-route-path': '/planes',
},
deprecated_resource: {
type: 'object',
Expand All @@ -83,6 +85,7 @@ export default {
required: ['deprecated_resource_id'],
deprecated: true,
'x-deprecated': 'This resource is deprecated',
'x-route-path': '/deprecated/resources',
},
draft_resource: {
type: 'object',
Expand All @@ -96,6 +99,7 @@ export default {
},
required: ['draft_resource_id'],
'x-draft': 'This resource is draft',
'x-route-path': '/draft/resources',
},
undocumented_resource: {
type: 'object',
Expand All @@ -109,6 +113,39 @@ export default {
},
required: ['undocumented_resource_id'],
'x-undocumented': 'This resource is undocumented',
'x-route-path': '/undocumented/resources',
},
event: {
oneOf: [
{
type: 'object',
description: 'A foo.created event',
properties: {
event_id: {
description: 'Event ID',
format: 'uuid',
type: 'string',
},
event_type: {
description: 'Type of event',
type: 'string',
enum: ['foo.created'],
},
foo_id: {
description: 'ID of the foo that was created',
format: 'uuid',
type: 'string',
},
created_at: {
description: 'When the event occurred',
type: 'string',
format: 'date-time',
},
},
required: ['event_id', 'event_type', 'foo_id', 'created_at'],
'x-route-path': '/foos',
},
],
},
},
},
Expand Down
Loading

0 comments on commit 8e09da6

Please sign in to comment.