Skip to content

Commit

Permalink
feat: siblings in parser-zod for better AST manipulation
Browse files Browse the repository at this point in the history
  • Loading branch information
Christoffer N authored and Christoffer N committed Oct 11, 2024
1 parent f53268f commit 60478fe
Show file tree
Hide file tree
Showing 6 changed files with 50 additions and 34 deletions.
5 changes: 5 additions & 0 deletions .changeset/three-tigers-taste.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@kubb/plugin-zod": patch
---

Add siblings to parser-zod for better manipulation of the AST
6 changes: 6 additions & 0 deletions packages/plugin-zod/mocks/petStore.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ paths:
required: false
schema:
type: string
- name: offset
in: query
required: false
schema:
type: integer
default: 0
responses:
'200':
description: A paged array of pets
Expand Down
8 changes: 3 additions & 5 deletions packages/plugin-zod/src/components/Zod.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,21 +19,19 @@ type Props = {

export function Zod({ name, typeName, tree, inferTypeName, mapper, coercion, keysToOmit, description }: Props): KubbNode {
const hasTuple = tree.some((item) => isKeyword(item, schemaKeywords.tuple))
const hasDefault = tree.some((item) => isKeyword(item, schemaKeywords.default))

const output = parserZod
.sort(tree)
.filter((item) => {
if (hasTuple && (isKeyword(item, schemaKeywords.min) || isKeyword(item, schemaKeywords.max))) {
return false
}
if (hasDefault && isKeyword(item, schemaKeywords.optional)) {
return false
}

return true
})
.map((item) => parserZod.parse(undefined, item, { name, keysToOmit, typeName: typeName, description, mapper, coercion }))
.map((item, _index, siblings) =>
parserZod.parse({ parent: undefined, current: item, siblings }, { name, keysToOmit, typeName: typeName, description, mapper, coercion }),
)
.filter(Boolean)
.join('')

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { z } from "zod";

export const listPetsQueryParams = z.object({ "limit": z.string().describe("How many items to return at one time (max 100)").optional() }).optional();
export const listPetsQueryParams = z.object({ "limit": z.string().describe("How many items to return at one time (max 100)").optional(), "offset": z.number().int().default(0) }).optional();

/**
* @description A paged array of pets
Expand Down
2 changes: 1 addition & 1 deletion packages/plugin-zod/src/parser/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import * as parserZod from './index.ts'

describe('zod parse', () => {
test.each(schemas.basic)('$name', ({ name, schema }) => {
const text = parserZod.parse(undefined, schema, { name })
const text = parserZod.parse({ parent: undefined, current: schema, siblings: [schema] }, { name })
expect(text).toMatchSnapshot()
})
})
61 changes: 34 additions & 27 deletions packages/plugin-zod/src/parser/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,17 +161,22 @@ const shouldCoerce = (coercion: ParserOptions['coercion'] | undefined, type: 'da
return !!coercion[type]
}

type SchemaTree = {
parent: Schema | undefined
current: Schema
siblings: Schema[]
}

type ParserOptions = {
name: string
typeName?: string
description?: string

keysToOmit?: string[]
mapper?: Record<string, string>
coercion?: boolean | { dates?: boolean; strings?: boolean; numbers?: boolean }
}

export function parse(parent: Schema | undefined, current: Schema, options: ParserOptions): string | undefined {
export function parse({ parent, current, siblings }: SchemaTree, options: ParserOptions): string | undefined {
const value = zodKeywordMapper[current.keyword as keyof typeof zodKeywordMapper]

if (!value) {
Expand All @@ -181,15 +186,15 @@ export function parse(parent: Schema | undefined, current: Schema, options: Pars
if (isKeyword(current, schemaKeywords.union)) {
// zod union type needs at least 2 items
if (Array.isArray(current.args) && current.args.length === 1) {
return parse(parent, current.args[0] as Schema, options)
return parse({ parent, current: current.args[0] as Schema, siblings }, options)
}
if (Array.isArray(current.args) && !current.args.length) {
return ''
}

return zodKeywordMapper.union(
sort(current.args)
.map((schema) => parse(current, schema, options))
.map((schema, _index, siblings) => parse({ parent: current, current: schema, siblings }, options))
.filter(Boolean),
)
}
Expand All @@ -199,7 +204,7 @@ export function parse(parent: Schema | undefined, current: Schema, options: Pars
.filter((schema: Schema) => {
return ![schemaKeywords.optional, schemaKeywords.describe].includes(schema.keyword as typeof schemaKeywords.describe)
})
.map((schema: Schema) => parse(current, schema, options))
.map((schema: Schema, _index, siblings) => parse({ parent: current, current: schema, siblings }, options))
.filter(Boolean)

return `${items.slice(0, 1)}${zodKeywordMapper.and(items.slice(1))}`
Expand All @@ -208,7 +213,7 @@ export function parse(parent: Schema | undefined, current: Schema, options: Pars
if (isKeyword(current, schemaKeywords.array)) {
return zodKeywordMapper.array(
sort(current.args.items)
.map((schemas) => parse(current, schemas, options))
.map((schemas, _index, siblings) => parse({ parent: current, current: schemas, siblings }, options))
.filter(Boolean),
current.args.min,
current.args.max,
Expand All @@ -218,27 +223,21 @@ export function parse(parent: Schema | undefined, current: Schema, options: Pars
if (isKeyword(current, schemaKeywords.enum)) {
if (current.args.asConst) {
if (current.args.items.length === 1) {
return parse(
current,
{
keyword: schemaKeywords.const,
args: current.args.items[0],
},
options,
)
const child = {
keyword: schemaKeywords.const,
args: current.args.items[0],
}
return parse({ parent: current, current: child, siblings: [child] }, options)
}

return zodKeywordMapper.union(
current.args.items
.map((schema) => {
return parse(
current,
{
keyword: schemaKeywords.const,
args: schema,
},
options,
)
.map((schema) => ({
keyword: schemaKeywords.const,
args: schema,
}))
.map((schema, _index, siblings) => {
return parse({ parent: current, current: schema, siblings }, options)
})
.filter(Boolean),
)
Expand Down Expand Up @@ -278,8 +277,8 @@ export function parse(parent: Schema | undefined, current: Schema, options: Pars
}

return `"${name}": ${sort(schemas)
.map((schema, array) => {
return parse(current, schema, options)
.map((schema, array, siblings) => {
return parse({ parent: current, current: schema, siblings }, options)
})
.filter(Boolean)
.join('')}`
Expand All @@ -288,7 +287,7 @@ export function parse(parent: Schema | undefined, current: Schema, options: Pars

const additionalProperties = current.args?.additionalProperties?.length
? current.args.additionalProperties
.map((schema) => parse(current, schema, options))
.map((schema, _index, siblings) => parse({ parent: current, current: schema, siblings }, options))
.filter(Boolean)
.join('')
: undefined
Expand All @@ -303,7 +302,9 @@ export function parse(parent: Schema | undefined, current: Schema, options: Pars
}

if (isKeyword(current, schemaKeywords.tuple)) {
return zodKeywordMapper.tuple(current.args.items.map((schema) => parse(current, schema, options)).filter(Boolean))
return zodKeywordMapper.tuple(
current.args.items.map((schema, _index, siblings) => parse({ parent: current, current: schema, siblings }, options)).filter(Boolean),
)
}

if (isKeyword(current, schemaKeywords.const)) {
Expand Down Expand Up @@ -372,6 +373,12 @@ export function parse(parent: Schema | undefined, current: Schema, options: Pars
return value((current as SchemaKeywordBase<unknown>).args as any)
}

if (isKeyword(current, schemaKeywords.optional)) {
if (siblings.some((schema) => isKeyword(schema, schemaKeywords.default))) return ''

return value()
}

if (current.keyword in zodKeywordMapper) {
return value()
}
Expand Down

0 comments on commit 60478fe

Please sign in to comment.