Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: siblings in parser-zod for better AST manipulation #1342

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading