Skip to content

Commit

Permalink
fix: Correctly define request body params on oneOf/allOf schemas (#158)
Browse files Browse the repository at this point in the history
Co-authored-by: Seam Bot <seambot@getseam.com>
  • Loading branch information
andrii-balitskyi and seambot authored Feb 20, 2025
1 parent 1eacbb0 commit eb6efd9
Show file tree
Hide file tree
Showing 6 changed files with 3,076 additions and 353 deletions.
37 changes: 20 additions & 17 deletions src/lib/blueprint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import type {
CodeSampleSyntax,
} from './code-sample/schema.js'
import { findCommonOpenapiSchemaProperties } from './openapi/find-common-openapi-schema-properties.js'
import { flattenOpenapiSchema } from './openapi/flatten-openapi-schema.js'
import {
type AuthMethodSchema,
EventResourceSchema,
Expand Down Expand Up @@ -344,7 +345,7 @@ const extractValidActionAttemptTypes = (

const processedActionAttemptTypes = new Set<string>()
actionAttemptSchema.oneOf.forEach((schema) => {
const actionType = schema.properties?.action_type?.enum?.[0]
const actionType = schema.properties?.['action_type']?.enum?.[0]
if (typeof actionType === 'string') {
processedActionAttemptTypes.add(actionType)
}
Expand Down Expand Up @@ -700,19 +701,19 @@ const createRequestBody = (
}

const requestBody = operation.requestBody
const jsonSchema = requestBody.content?.['application/json']?.schema
if (jsonSchema == null) return []

if (
requestBody.content?.['application/json']?.schema?.properties === undefined
)
return []

const schema = requestBody.content['application/json'].schema

if (schema.type !== 'object' || schema.properties == null) {
const flattenedSchema = flattenOpenapiSchema(jsonSchema)
if (flattenedSchema.type !== 'object' || flattenedSchema.properties == null) {
return []
}

return createParameters(schema.properties, path, schema.required)
return createParameters(
flattenedSchema.properties,
path,
flattenedSchema.required,
)
}

const createParameters = (
Expand Down Expand Up @@ -1202,18 +1203,18 @@ const createEvents = (
.map((schema) => {
if (
typeof schema !== 'object' ||
schema.properties?.event_type?.enum?.[0] == null
schema.properties?.['event_type']?.enum?.[0] == null
) {
return null
}

const eventType = schema.properties.event_type.enum[0]
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, routes),
...createResource('event', schema, routes),
eventType,
targetResourceType: targetResourceType ?? null,
}
Expand Down Expand Up @@ -1241,25 +1242,27 @@ const createActionAttempts = (
.map((schema) => {
if (
typeof schema !== 'object' ||
schema.properties?.action_type?.enum?.[0] == null
schema.properties?.['action_type']?.enum?.[0] == null
) {
return null
}

const actionType = schema.properties.action_type.enum[0] as string
const actionType = schema.properties['action_type'].enum[0]

if (processedActionTypes.has(actionType)) {
return null
}
processedActionTypes.add(actionType)

const schemaWithStandardStatus: OpenapiSchema = {
'x-route-path': actionAttemptSchema['x-route-path'],
...(actionAttemptSchema['x-route-path'] !== null && {
'x-route-path': actionAttemptSchema['x-route-path'],
}),
...schema,
properties: {
...schema.properties,
status: {
...schema.properties.status,
...schema.properties['status'],
type: 'string',
enum: ['success', 'pending', 'error'],
},
Expand Down
202 changes: 202 additions & 0 deletions src/lib/openapi/flatten-openapi-schema.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
import test from 'ava'

import {
flattenAllOfSchema,
flattenOneOfSchema,
flattenOpenapiSchema,
} from './flatten-openapi-schema.js'
import type { OpenapiSchema } from './types.js'

test('flattenOpenapiSchema: returns schema unchanged if no allOf/oneOf', (t) => {
const schema: OpenapiSchema = { type: 'string', enum: ['foo'] }
const flattened = flattenOpenapiSchema(schema)
t.deepEqual(flattened, schema)
})

test('flattenAllOfSchema: merges properties and required fields', (t) => {
const schema = {
allOf: [
{
type: 'object',
properties: { a: { type: 'string', description: 'desc A' } },
required: ['a'],
},
{
type: 'object',
properties: { b: { type: 'number', description: 'desc B' } },
required: ['b'],
},
],
}
const flattened = flattenAllOfSchema(schema as { allOf: OpenapiSchema[] })

t.is(flattened.type, 'object')
t.truthy(flattened.properties)
t.deepEqual(flattened.properties, {
a: { type: 'string', description: 'desc A' },
b: { type: 'number', description: 'desc B' },
})
t.deepEqual(flattened.required, ['a', 'b'])
})

test('flattenOneOfSchema (string enums): merges enums and deduplicates', (t) => {
const schema: OpenapiSchema = {
oneOf: [
{ type: 'string', enum: ['foo', 'bar'] },
{ type: 'string', enum: ['bar', 'baz'] },
],
}
const flattened = flattenOneOfSchema(schema as { oneOf: OpenapiSchema[] })
t.is(flattened.type, 'string')
t.deepEqual(flattened.enum, ['foo', 'bar', 'baz'])
})

test('flattenOneOfSchema (object merging): merges properties and computes required intersection', (t) => {
const schema: OpenapiSchema = {
oneOf: [
{
type: 'object',
properties: { a: { type: 'string' } },
required: ['a'],
},
{
type: 'object',
properties: { a: { type: 'string' }, b: { type: 'number' } },
required: ['a', 'b'],
},
],
}
const flattened = flattenOneOfSchema(schema as { oneOf: OpenapiSchema[] })
t.is(flattened.type, 'object')
t.deepEqual(flattened.properties, {
a: { type: 'string' },
b: { type: 'number' },
})
// Intersection of ['a'] and ['a', 'b'] should yield ['a'].
t.deepEqual(flattened.required, ['a'])
})

test('flattenOpenapiSchema: recursively flattens nested properties', (t) => {
// Create an object schema that has a nested property using allOf.
const schema: OpenapiSchema = {
type: 'object',
properties: {
foo: {
allOf: [
{
type: 'object',
properties: { a: { type: 'string' } },
required: ['a'],
},
{
type: 'object',
properties: { b: { type: 'number' } },
required: ['b'],
},
],
},
},
}
const flattened = flattenOpenapiSchema(schema)
t.is(flattened.type, 'object')
t.truthy(flattened.properties)
t.deepEqual(flattened.properties?.['foo'], {
type: 'object',
properties: {
a: { type: 'string' },
b: { type: 'number' },
},
required: ['a', 'b'],
})
})

test('flattenOpenapiSchema: oneOf nests allOf', (t) => {
const schema: OpenapiSchema = {
oneOf: [
{
allOf: [
{
type: 'object',
properties: { a: { type: 'string' } },
required: ['a'],
},
{
type: 'object',
properties: { b: { type: 'number' } },
required: ['b'],
},
],
},
{
allOf: [
{
type: 'object',
properties: { a: { type: 'string' } },
required: ['a'],
},
{
type: 'object',
properties: { c: { type: 'boolean' } },
required: ['c'],
},
],
},
],
}
const flattened = flattenOpenapiSchema(schema)
t.deepEqual(flattened, {
type: 'object',
properties: {
a: { type: 'string' },
b: { type: 'number' },
c: { type: 'boolean' },
},
required: ['a'],
})
})

test('flattenOpenapiSchema: allOf with two oneOf schemas', (t) => {
const schema: OpenapiSchema = {
allOf: [
{
oneOf: [
{
type: 'object',
properties: { a: { type: 'string' } },
required: ['a'],
},
{
type: 'object',
properties: { b: { type: 'number' } },
required: ['b'],
},
],
},
{
oneOf: [
{
type: 'object',
properties: { c: { type: 'boolean' } },
required: ['c'],
},
{
type: 'object',
properties: { d: { type: 'string' } },
required: ['d'],
},
],
},
],
}
const flattened = flattenOpenapiSchema(schema)
t.deepEqual(flattened, {
type: 'object',
properties: {
a: { type: 'string' },
b: { type: 'number' },
c: { type: 'boolean' },
d: { type: 'string' },
},
required: [],
})
})
Loading

0 comments on commit eb6efd9

Please sign in to comment.