fork of hey-api/openapi-ts because I need some additional things
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Merge pull request #3440 from hey-api/chore/py-dsl-types

chore: clean up some assertions

authored by

Lubos and committed by
GitHub
f3253660 14f2894c

+844 -429
+1 -1
dev/inputs.ts
··· 19 19 export type InputKey = keyof typeof inputs; 20 20 21 21 export function getInput(key: InputKey = (process.env.INPUT as InputKey) || 'opencode') { 22 - const input = inputs[key]; 22 + const input = inputs[key] || key; 23 23 if (!input) { 24 24 throw new Error(`Unknown input: ${key}. Available: ${Object.keys(inputs).join(', ')}`); 25 25 }
+27 -52
packages/openapi-python/src/plugins/pydantic/shared/export.ts
··· 5 5 import type { ProcessorContext } from './processor'; 6 6 // import { identifiers } from '../v2/constants'; 7 7 // import { pipesToNode } from './pipes'; 8 - import type { Ast, IrSchemaToAstOptions } from './types'; 8 + import type { PydanticFinal } from './types'; 9 9 10 10 export function exportAst({ 11 + final, 11 12 meta, 12 13 naming, 13 14 namingAnchor, 14 15 path, 15 16 plugin, 16 17 tags, 17 - }: Pick<IrSchemaToAstOptions, 'state'> & 18 - ProcessorContext & { 19 - ast: Ast; 20 - }): void { 18 + }: ProcessorContext & { 19 + final: PydanticFinal; 20 + }): void { 21 21 const name = pathToName(path, { anchor: namingAnchor }); 22 22 const symbol = plugin.symbol(applyNaming(name, naming), { 23 23 meta: { ··· 29 29 }, 30 30 }); 31 31 32 - const baseModel = plugin.external('pydantic.BaseModel'); 33 - const classDef = $.class(symbol).extends(baseModel); 34 - // .export() 35 - // .$if(plugin.config.comments && createSchemaComment(schema), (c, v) => c.doc(v)) 36 - // .$if(state.hasLazyExpression['~ref'], (c) => 37 - // c.type($.type(v).attr(ast.typeName || identifiers.types.GenericSchema)), 38 - // ) 39 - // .assign(pipesToNode(ast.pipes, plugin)); 40 - plugin.node(classDef); 41 - // if (schema.type === 'object' && schema.properties) { 42 - // const baseModelSymbol = plugin.external('pydantic.BaseModel'); 43 - // const fieldSymbol = plugin.external('pydantic.Field'); 44 - // const classDef = $.class(symbol).extends(baseModelSymbol); 32 + if (final.fields) { 33 + const baseModel = plugin.external('pydantic.BaseModel'); 34 + const classDef = $.class(symbol).extends(baseModel); 35 + // .export() 36 + // .$if(plugin.config.comments && createSchemaComment(schema), (c, v) => c.doc(v)) 37 + // .$if(state.hasLazyExpression['~ref'], (c) => 38 + // c.type($.type(v).attr(ast.typeName || identifiers.types.GenericSchema)), 39 + // ) 40 + // .assign(pipesToNode(ast.pipes, plugin)); 45 41 46 - // if (plugin.config.comments && schema.description) { 47 - // classDef.doc(schema.description); 48 - // } 42 + for (const field of final.fields) { 43 + // TODO: Field(...) constraints in next pass 44 + classDef.do($.var(field.name).assign($.literal('hey'))); 45 + // classDef.do($.var(field.name).annotate(field.typeAnnotation)); 46 + } 49 47 50 - // for (const name in schema.properties) { 51 - // const property = schema.properties[name]!; 52 - // const isOptional = !schema.required?.includes(name); 48 + plugin.node(classDef); 49 + } else { 50 + const statement = $.var(symbol) 51 + // .export() 52 + // .$if(plugin.config.comments && createSchemaComment(schema), (c, v) => c.doc(v)) 53 + .assign(final.typeAnnotation); 53 54 54 - // const propertyAst = irSchemaToAst({ 55 - // optional: isOptional, 56 - // plugin, 57 - // schema: property, 58 - // state: { 59 - // ...state, 60 - // path: ref([...fromRef(state.path), 'properties', name]), 61 - // }, 62 - // }); 63 - 64 - // let typeAnnotation = propertyAst.typeAnnotation; 65 - 66 - // if (isOptional && !typeAnnotation.startsWith('Optional[')) { 67 - // typeAnnotation = `Optional[${typeAnnotation}]`; 68 - // } 69 - 70 - // if (propertyAst.fieldConstraints && Object.keys(propertyAst.fieldConstraints).length > 0) { 71 - // const constraints = Object.entries(propertyAst.fieldConstraints) 72 - // .map(([key, value]) => `${key}=${JSON.stringify(value)}`) 73 - // .join(', '); 74 - // classDef.do($.stmt($.expr(`${name}: ${typeAnnotation} = Field(${constraints})`))); 75 - // } else { 76 - // classDef.do($.stmt($.expr(`${name}: ${typeAnnotation}`))); 77 - // } 78 - // } 79 - 80 - // plugin.node(classDef); 81 - // } 55 + plugin.node(statement); 56 + } 82 57 }
+55
packages/openapi-python/src/plugins/pydantic/shared/meta.ts
··· 1 + import type { IR } from '@hey-api/shared'; 2 + 3 + import type { PydanticMeta, PydanticResult } from './types'; 4 + 5 + /** 6 + * Creates default metadata from a schema. 7 + */ 8 + export function defaultMeta(schema: IR.SchemaObject): PydanticMeta { 9 + return { 10 + default: schema.default, 11 + format: schema.format, 12 + hasLazy: false, 13 + nullable: false, 14 + readonly: schema.accessScope === 'read', 15 + }; 16 + } 17 + 18 + /** 19 + * Composes metadata from child results. 20 + * 21 + * Automatically propagates hasLazy, nullable, readonly from children. 22 + * 23 + * @param children - Results from walking child schemas 24 + * @param overrides - Explicit overrides (e.g., from parent schema) 25 + */ 26 + export function composeMeta( 27 + children: ReadonlyArray<PydanticResult>, 28 + overrides?: Partial<PydanticMeta>, 29 + ): PydanticMeta { 30 + return { 31 + default: overrides?.default, 32 + format: overrides?.format, 33 + hasLazy: overrides?.hasLazy ?? children.some((c) => c.meta.hasLazy), 34 + nullable: overrides?.nullable ?? children.some((c) => c.meta.nullable), 35 + readonly: overrides?.readonly ?? children.some((c) => c.meta.readonly), 36 + }; 37 + } 38 + 39 + /** 40 + * Merges parent schema metadata with composed child metadata. 41 + * 42 + * @param parent - The parent schema 43 + * @param children - Results from walking child schemas 44 + */ 45 + export function inheritMeta( 46 + parent: IR.SchemaObject, 47 + children: ReadonlyArray<PydanticResult>, 48 + ): PydanticMeta { 49 + return composeMeta(children, { 50 + default: parent.default, 51 + format: parent.format, 52 + nullable: false, 53 + readonly: parent.accessScope === 'read', 54 + }); 55 + }
+55 -1
packages/openapi-python/src/plugins/pydantic/shared/types.ts
··· 1 1 import type { Refs, Symbol, SymbolMeta } from '@hey-api/codegen-core'; 2 2 import type { IR, SchemaExtractor } from '@hey-api/shared'; 3 3 4 - import type { $ } from '../../../py-dsl'; 4 + import type { $, MaybePyDsl } from '../../../py-dsl'; 5 + import type { py } from '../../../ts-python'; 5 6 import type { PydanticPlugin } from '../types'; 6 7 import type { ProcessorContext } from './processor'; 7 8 ··· 65 66 */ 66 67 schema: IR.SchemaObject; 67 68 } 69 + 70 + // ..... ^^^^^^ OLD 71 + 72 + /** 73 + * Metadata that flows through schema walking. 74 + */ 75 + export interface PydanticMeta { 76 + /** Default value from schema. */ 77 + default?: unknown; 78 + /** Original format (for BigInt coercion). */ 79 + format?: string; 80 + /** Whether this or any child contains a lazy reference. */ 81 + hasLazy: boolean; 82 + /** Does this schema explicitly allow null? */ 83 + nullable: boolean; 84 + /** Is this schema read-only? */ 85 + readonly: boolean; 86 + } 87 + 88 + /** 89 + * Result from walking a schema node. 90 + */ 91 + export interface PydanticResult { 92 + fieldConstraints: Record<string, unknown>; 93 + fields?: Array<PydanticField>; 94 + meta: PydanticMeta; 95 + typeAnnotation: string | MaybePyDsl<py.Expression>; 96 + } 97 + 98 + export interface PydanticField { 99 + fieldConstraints: Record<string, unknown>; 100 + isOptional: boolean; 101 + name: string; 102 + typeAnnotation: string | MaybePyDsl<py.Expression>; 103 + } 104 + 105 + /** 106 + * Finalized result after applyModifiers. 107 + */ 108 + export interface PydanticFinal { 109 + fieldConstraints: Record<string, unknown>; 110 + fields?: Array<PydanticField>; // present = emit class, absent = emit type alias 111 + typeAnnotation: string | MaybePyDsl<py.Expression>; 112 + } 113 + 114 + /** 115 + * Result from composite handlers that walk children. 116 + */ 117 + export interface PydanticCompositeHandlerResult { 118 + childResults: Array<PydanticResult>; 119 + fieldConstraints: Record<string, unknown>; 120 + typeAnnotation: string | MaybePyDsl<py.Expression>; 121 + }
+1 -118
packages/openapi-python/src/plugins/pydantic/v2/plugin.ts
··· 1 - import type { SymbolMeta } from '@hey-api/codegen-core'; 2 - import { fromRef, ref } from '@hey-api/codegen-core'; 3 - import type { IR, SchemaWithType } from '@hey-api/shared'; 4 - import { deduplicateSchema, pathToJsonPointer } from '@hey-api/shared'; 1 + import { pathToJsonPointer } from '@hey-api/shared'; 5 2 6 3 // import { $ } from '../../../py-dsl'; 7 - import type { Ast, IrSchemaToAstOptions } from '../shared/types'; 8 4 import type { PydanticPlugin } from '../types'; 9 5 import { createProcessor } from './processor'; 10 - import { irSchemaWithTypeToAst } from './toAst'; 11 - 12 - export function irSchemaToAst({ 13 - optional, 14 - plugin, 15 - schema, 16 - schemaExtractor, 17 - state, 18 - }: IrSchemaToAstOptions & { 19 - optional?: boolean; 20 - schema: IR.SchemaObject; 21 - }): Ast { 22 - if (schemaExtractor && !schema.$ref) { 23 - const extracted = schemaExtractor({ 24 - meta: { 25 - resource: 'definition', 26 - resourceId: pathToJsonPointer(fromRef(state.path)), 27 - }, 28 - naming: plugin.config.definitions, 29 - path: fromRef(state.path), 30 - plugin, 31 - schema, 32 - }); 33 - if (extracted !== schema) schema = extracted; 34 - } 35 - 36 - if (schema.$ref) { 37 - const query: SymbolMeta = { 38 - category: 'schema', 39 - resource: 'definition', 40 - resourceId: schema.$ref, 41 - tool: 'pydantic', 42 - }; 43 - const refSymbol = plugin.referenceSymbol(query); 44 - const refName = typeof refSymbol === 'string' ? refSymbol : refSymbol.name; 45 - 46 - return { 47 - // expression: $.expr(refName), 48 - fieldConstraints: optional ? { default: null } : undefined, 49 - hasLazyExpression: !plugin.isSymbolRegistered(query), 50 - models: [], 51 - // pipes: [], 52 - typeAnnotation: refName, 53 - }; 54 - } 55 - 56 - if (schema.type) { 57 - const typeAst = irSchemaWithTypeToAst({ 58 - plugin, 59 - schema: schema as SchemaWithType, 60 - state, 61 - }); 62 - 63 - const constraints: Record<string, unknown> = {}; 64 - if (optional) { 65 - constraints.default = null; 66 - } 67 - if (schema.default !== undefined) { 68 - constraints.default = schema.default; 69 - } 70 - if (schema.description) { 71 - constraints.description = schema.description; 72 - } 73 - 74 - return { 75 - ...typeAst, 76 - fieldConstraints: { ...typeAst.fieldConstraints, ...constraints }, 77 - // pipes: [], 78 - }; 79 - } 80 - 81 - if (schema.items) { 82 - schema = deduplicateSchema({ schema }); 83 - 84 - if (schema.items) { 85 - const itemsAnnotations: string[] = []; 86 - const itemsConstraints: Record<string, unknown>[] = []; 87 - 88 - for (const item of schema.items) { 89 - const itemAst = irSchemaToAst({ 90 - plugin, 91 - schema: item, 92 - state: { 93 - ...state, 94 - path: ref([...fromRef(state.path), 'items']), 95 - }, 96 - }); 97 - itemsAnnotations.push(itemAst.typeAnnotation); 98 - if (itemAst.fieldConstraints) { 99 - itemsConstraints.push(itemAst.fieldConstraints); 100 - } 101 - } 102 - 103 - const unionType = itemsAnnotations.join(' | '); 104 - return { 105 - // expression: $.expr(`list[${unionType}]`), 106 - fieldConstraints: itemsConstraints.length > 0 ? itemsConstraints[0] : undefined, 107 - hasLazyExpression: false, 108 - models: [], 109 - // pipes: [], 110 - typeAnnotation: `list[${unionType}]`, 111 - }; 112 - } 113 - } 114 - 115 - return { 116 - // expression: $.expr('Any'), 117 - hasLazyExpression: false, 118 - models: [], 119 - // pipes: [], 120 - typeAnnotation: 'Any', 121 - }; 122 - } 123 6 124 7 export const handlerV2: PydanticPlugin['Handler'] = ({ plugin }) => { 125 8 plugin.symbol('Any', {
+14 -14
packages/openapi-python/src/plugins/pydantic/v2/processor.ts
··· 1 - import { refs } from '@hey-api/codegen-core'; 1 + import { ref } from '@hey-api/codegen-core'; 2 2 import type { IR } from '@hey-api/shared'; 3 - import { createSchemaProcessor, pathToJsonPointer } from '@hey-api/shared'; 3 + import { createSchemaProcessor, createSchemaWalker, pathToJsonPointer } from '@hey-api/shared'; 4 4 5 5 import { exportAst } from '../shared/export'; 6 6 import type { ProcessorContext, ProcessorResult } from '../shared/processor'; 7 - import type { PluginState } from '../shared/types'; 7 + import type { PydanticFinal } from '../shared/types'; 8 8 import type { PydanticPlugin } from '../types'; 9 - import { irSchemaToAst } from './plugin'; 9 + import { createVisitor } from './walker'; 10 10 11 11 export function createProcessor(plugin: PydanticPlugin['Instance']): ProcessorResult { 12 12 const processor = createSchemaProcessor(); ··· 37 37 if (!processor.markEmitted(ctx.path)) return; 38 38 39 39 processor.withContext({ anchor: ctx.namingAnchor, tags: ctx.tags }, () => { 40 - const state = refs<PluginState>({ 41 - hasLazyExpression: false, 42 - path: ctx.path, 43 - tags: ctx.tags, 44 - }); 40 + const visitor = createVisitor({ schemaExtractor: extractor }); 41 + const walk = createSchemaWalker(visitor); 45 42 46 - const ast = irSchemaToAst({ 43 + const result = walk(ctx.schema, { 44 + path: ref(ctx.path), 47 45 plugin, 48 - schema: ctx.schema, 49 - schemaExtractor: extractor, 50 - state, 51 46 }); 52 47 53 - exportAst({ ...ctx, ast, plugin, state }); 48 + const final = visitor.applyModifiers(result, { 49 + path: ref(ctx.path), 50 + plugin, 51 + }) as PydanticFinal; 52 + 53 + exportAst({ ...ctx, final, plugin }); 54 54 }); 55 55 } 56 56
+28
packages/openapi-python/src/plugins/pydantic/v2/toAst/boolean.ts
··· 1 + import type { SchemaWithType } from '@hey-api/shared'; 2 + 3 + import { defaultMeta } from '../../shared/meta'; 4 + import type { PydanticResult } from '../../shared/types'; 5 + import type { PydanticPlugin } from '../../types'; 6 + 7 + export function booleanToType({ 8 + plugin, 9 + schema, 10 + }: { 11 + plugin: PydanticPlugin['Instance']; 12 + schema: SchemaWithType<'boolean'>; 13 + }): PydanticResult { 14 + if (typeof schema.const === 'boolean') { 15 + const literal = plugin.external('typing.Literal'); 16 + return { 17 + fieldConstraints: {}, 18 + meta: defaultMeta(schema), 19 + typeAnnotation: `${literal}[${schema.const ? 'True' : 'False'}]`, 20 + }; 21 + } 22 + 23 + return { 24 + fieldConstraints: {}, 25 + meta: defaultMeta(schema), 26 + typeAnnotation: 'bool', 27 + }; 28 + }
-31
packages/openapi-python/src/plugins/pydantic/v2/toAst/index.ts
··· 1 - import type { SchemaWithType } from '@hey-api/shared'; 2 - 3 - import type { Ast, IrSchemaToAstOptions } from '../../shared/types'; 4 - import { objectToAst } from './object'; 5 - import { stringToNode } from './string'; 6 - 7 - export function irSchemaWithTypeToAst({ 8 - schema, 9 - ...args 10 - }: IrSchemaToAstOptions & { 11 - schema: SchemaWithType; 12 - }): Ast { 13 - switch (schema.type) { 14 - case 'object': 15 - return objectToAst({ 16 - ...args, 17 - schema: schema as SchemaWithType<'object'>, 18 - }); 19 - case 'string': 20 - return stringToNode({ 21 - ...args, 22 - schema: schema as SchemaWithType<'string'>, 23 - }); 24 - default: 25 - return { 26 - // expression: 'Any', 27 - models: [], 28 - typeAnnotation: 'Any', 29 - }; 30 - } 31 - }
+98 -51
packages/openapi-python/src/plugins/pydantic/v2/toAst/object.ts
··· 1 - // import { fromRef, ref } from '@hey-api/codegen-core'; 2 - import type { SchemaWithType } from '@hey-api/shared'; 1 + import type { SchemaVisitorContext, SchemaWithType, Walker } from '@hey-api/shared'; 2 + import { childContext } from '@hey-api/shared'; 3 3 4 - import { $ } from '../../../../py-dsl'; 5 - import type { Ast, IrSchemaToAstOptions } from '../../shared/types'; 6 - // import { irSchemaToAst } from '../plugin'; 4 + import { $, type MaybePyDsl } from '../../../../py-dsl'; 5 + import type { py } from '../../../../ts-python'; 6 + import type { PydanticField, PydanticFinal, PydanticResult } from '../../shared/types'; 7 + import type { PydanticPlugin } from '../../types'; 7 8 8 - export function objectToAst({ 9 - plugin, 10 - // schema, 11 - // state, 12 - }: IrSchemaToAstOptions & { 9 + interface ObjectResolverContext { 10 + _childResults: Array<PydanticResult>; 11 + applyModifiers: (result: PydanticResult, options?: { optional?: boolean }) => PydanticFinal; 12 + plugin: PydanticPlugin['Instance']; 13 13 schema: SchemaWithType<'object'>; 14 - }): Ast { 15 - const symbolBaseModel = plugin.external('pydantic.BaseModel'); 16 - // const fieldSymbol = plugin.external('pydantic.Field'); 17 - const symbolTemp = plugin.symbol('temp'); 14 + walk: Walker<PydanticResult, PydanticPlugin['Instance']>; 15 + walkerCtx: SchemaVisitorContext<PydanticPlugin['Instance']>; 16 + } 17 + 18 + export interface ObjectToFieldsResult { 19 + childResults: Array<PydanticResult>; 20 + fields?: Array<PydanticField>; // present = emit class 21 + typeAnnotation?: string | MaybePyDsl<py.Expression>; // present = emit type alias (dict case) 22 + } 23 + 24 + function resolveAdditionalProperties( 25 + ctx: ObjectResolverContext, 26 + ): string | MaybePyDsl<py.Expression> | null | undefined { 27 + const { schema } = ctx; 28 + 29 + if (!schema.additionalProperties || !schema.additionalProperties.type) return undefined; 30 + if (schema.additionalProperties.type === 'never') return null; 31 + 32 + const result = ctx.walk( 33 + schema.additionalProperties, 34 + childContext(ctx.walkerCtx, 'additionalProperties'), 35 + ); 36 + ctx._childResults.push(result); 37 + 38 + return result.typeAnnotation; 39 + } 40 + 41 + function resolveFields(ctx: ObjectResolverContext): Array<PydanticField> { 42 + const { schema } = ctx; 43 + const fields: Array<PydanticField> = []; 44 + 45 + for (const name in schema.properties) { 46 + const property = schema.properties[name]!; 47 + const isOptional = !schema.required?.includes(name); 48 + 49 + const propertyResult = ctx.walk(property, childContext(ctx.walkerCtx, 'properties', name)); 50 + ctx._childResults.push(propertyResult); 18 51 19 - const classDef = $.class(symbolTemp).extends(symbolBaseModel); 20 - console.log(classDef); 52 + const final = ctx.applyModifiers(propertyResult, { optional: isOptional }); 53 + fields.push({ 54 + fieldConstraints: final.fieldConstraints, 55 + isOptional, 56 + name, 57 + typeAnnotation: final.typeAnnotation, 58 + }); 59 + } 60 + 61 + return fields; 62 + } 63 + 64 + function objectResolver(ctx: ObjectResolverContext): Omit<ObjectToFieldsResult, 'childResults'> { 65 + const additional = resolveAdditionalProperties(ctx); 21 66 22 - // if (schema.properties) { 23 - // for (const name in schema.properties) { 24 - // const property = schema.properties[name]!; 25 - // const isOptional = !schema.required?.includes(name); 67 + // additionalProperties: false → strict — still emit a class, just no extra fields 68 + // additionalProperties: { type: 'never' } → null sentinel → strict class 69 + if (additional === null) { 70 + const fields = resolveFields(ctx); 71 + return { fields }; 72 + } 26 73 27 - // const propertyAst = irSchemaToAst({ 28 - // optional: isOptional, 29 - // plugin, 30 - // schema: property, 31 - // state: { 32 - // ...state, 33 - // path: ref([...fromRef(state.path), 'properties', name]), 34 - // }, 35 - // }); 74 + if (additional && !ctx.schema.properties) { 75 + const any = ctx.plugin.external('typing.Any'); 76 + return { typeAnnotation: $('dict').slice('str', any) }; 77 + } 36 78 37 - // let typeAnnotation = propertyAst.typeAnnotation; 79 + // additionalProperties with properties → class wins, additional props ignored for now 80 + // TODO: consider model_config = ConfigDict(extra='allow') 81 + if (ctx.schema.properties) { 82 + const fields = resolveFields(ctx); 83 + return { fields }; 84 + } 38 85 39 - // if (isOptional && !typeAnnotation.startsWith('Optional[')) { 40 - // typeAnnotation = `Optional[${typeAnnotation}]`; 41 - // } 86 + const any = ctx.plugin.external('typing.Any'); 87 + return { typeAnnotation: $('dict').slice('str', any) }; 88 + } 42 89 43 - // if (propertyAst.fieldConstraints && Object.keys(propertyAst.fieldConstraints).length > 0) { 44 - // const constraints = Object.entries(propertyAst.fieldConstraints) 45 - // .map(([key, value]) => `${key}=${JSON.stringify(value)}`) 46 - // .join(', '); 47 - // classDef.do($.expr(`${name}: ${typeAnnotation} = Field(${constraints})`)); 48 - // } else { 49 - // classDef.do($.expr(`${name}: ${typeAnnotation}`)); 50 - // } 51 - // } 52 - // } 90 + export function objectToFields( 91 + ctx: Pick<ObjectResolverContext, 'applyModifiers' | 'plugin' | 'schema' | 'walk' | 'walkerCtx'>, 92 + ): ObjectToFieldsResult { 93 + const { applyModifiers, plugin, schema, walk, walkerCtx } = ctx; 94 + const childResults: Array<PydanticResult> = []; 53 95 54 - return { 55 - // expression: classDef, 56 - fieldConstraints: {}, 57 - hasLazyExpression: false, 58 - models: [], 59 - // pipes: [], 60 - typeAnnotation: 'DynamicModel', 61 - typeName: 'DynamicModel', 96 + const extendedCtx: ObjectResolverContext = { 97 + _childResults: childResults, 98 + applyModifiers, 99 + plugin, 100 + schema, 101 + walk, 102 + walkerCtx, 62 103 }; 104 + 105 + // const resolver = plugin.config?.['~resolvers']?.object; 106 + // const resolved = resolver?.(extendedCtx) ?? objectResolver(extendedCtx); 107 + const resolved = objectResolver(extendedCtx); 108 + 109 + return { childResults, ...resolved }; 63 110 }
+186
packages/openapi-python/src/plugins/pydantic/v2/walker.ts
··· 1 + import type { SymbolMeta } from '@hey-api/codegen-core'; 2 + import { fromRef } from '@hey-api/codegen-core'; 3 + import type { SchemaExtractor, SchemaVisitor } from '@hey-api/shared'; 4 + import { pathToJsonPointer } from '@hey-api/shared'; 5 + 6 + import { composeMeta, defaultMeta, inheritMeta } from '../shared/meta'; 7 + import type { ProcessorContext } from '../shared/processor'; 8 + import type { PydanticFinal, PydanticResult } from '../shared/types'; 9 + import type { PydanticPlugin } from '../types'; 10 + import { booleanToType } from './toAst/boolean'; 11 + import { objectToFields } from './toAst/object'; 12 + 13 + export interface VisitorConfig { 14 + schemaExtractor?: SchemaExtractor<ProcessorContext>; 15 + } 16 + 17 + export function createVisitor( 18 + config: VisitorConfig, 19 + ): SchemaVisitor<PydanticResult, PydanticPlugin['Instance']> { 20 + const { schemaExtractor } = config; 21 + 22 + return { 23 + applyModifiers(result, ctx, options = {}): PydanticFinal { 24 + const { optional } = options; 25 + 26 + const hasDefault = result.meta.default !== undefined; 27 + const needsOptional = optional || hasDefault; 28 + const needsNullable = result.meta.nullable; 29 + 30 + let { typeAnnotation } = result; 31 + const fieldConstraints: Record<string, unknown> = { ...result.fieldConstraints }; 32 + 33 + if (needsOptional || needsNullable) { 34 + const optional = ctx.plugin.external('typing.Optional'); 35 + typeAnnotation = `${optional}[${typeAnnotation}]`; 36 + if (needsOptional) { 37 + fieldConstraints.default = hasDefault ? result.meta.default : null; 38 + } 39 + } 40 + 41 + return { fieldConstraints, fields: result.fields, typeAnnotation }; 42 + }, 43 + // @ts-expect-error 44 + array(schema, ctx) { 45 + return { 46 + fieldConstraints: {}, 47 + meta: defaultMeta(schema), 48 + typeAnnotation: ctx.plugin.external('typing.Any'), 49 + }; 50 + }, 51 + boolean(schema, ctx) { 52 + return booleanToType({ plugin: ctx.plugin, schema }); 53 + }, 54 + // @ts-expect-error 55 + enum(schema, ctx) { 56 + return { 57 + fieldConstraints: {}, 58 + meta: defaultMeta(schema), 59 + typeAnnotation: ctx.plugin.external('typing.Any'), 60 + }; 61 + }, 62 + integer(schema) { 63 + return { fieldConstraints: {}, meta: defaultMeta(schema), typeAnnotation: 'int' }; 64 + }, 65 + intercept(schema, ctx, walk) { 66 + if (schemaExtractor && !schema.$ref) { 67 + const extracted = schemaExtractor({ 68 + meta: { resource: 'definition', resourceId: pathToJsonPointer(fromRef(ctx.path)) }, 69 + naming: ctx.plugin.config.definitions, 70 + path: fromRef(ctx.path), 71 + plugin: ctx.plugin, 72 + schema, 73 + }); 74 + if (extracted !== schema) return walk(extracted, ctx); 75 + } 76 + }, 77 + // @ts-expect-error 78 + intersection(items, schemas, parentSchema, ctx) { 79 + return { 80 + fieldConstraints: {}, 81 + meta: composeMeta(items, { default: parentSchema.default }), 82 + typeAnnotation: ctx.plugin.external('typing.Any'), 83 + }; 84 + }, 85 + // @ts-expect-error 86 + never(schema, ctx) { 87 + return { 88 + fieldConstraints: {}, 89 + meta: { ...defaultMeta(schema), nullable: false, readonly: false }, 90 + typeAnnotation: ctx.plugin.external('typing.Any'), 91 + }; 92 + }, 93 + null(schema) { 94 + return { 95 + fieldConstraints: {}, 96 + meta: { ...defaultMeta(schema), nullable: false, readonly: false }, 97 + typeAnnotation: 'None', 98 + }; 99 + }, 100 + number(schema) { 101 + return { fieldConstraints: {}, meta: defaultMeta(schema), typeAnnotation: 'float' }; 102 + }, 103 + object(schema, ctx, walk) { 104 + const applyModifiers = (result: PydanticResult, opts?: { optional?: boolean }) => 105 + this.applyModifiers(result, ctx, opts) as PydanticFinal; 106 + 107 + const { childResults, fields, typeAnnotation } = objectToFields({ 108 + applyModifiers, 109 + plugin: ctx.plugin, 110 + schema, 111 + walk, 112 + walkerCtx: ctx, 113 + }); 114 + 115 + return { 116 + fieldConstraints: {}, 117 + fields, 118 + meta: inheritMeta(schema, childResults), 119 + typeAnnotation: typeAnnotation ?? '', 120 + }; 121 + }, 122 + postProcess(result) { 123 + return result; 124 + }, 125 + reference($ref, schema, ctx) { 126 + const query: SymbolMeta = { 127 + category: 'schema', 128 + resource: 'definition', 129 + resourceId: $ref, 130 + tool: 'pydantic', 131 + }; 132 + 133 + const refSymbol = ctx.plugin.referenceSymbol(query); 134 + const isRegistered = ctx.plugin.isSymbolRegistered(query); 135 + 136 + // TODO: replace string with symbol 137 + const refName = typeof refSymbol === 'string' ? refSymbol : refSymbol.name; 138 + return { 139 + fieldConstraints: {}, 140 + meta: { ...defaultMeta(schema), hasLazy: !isRegistered }, 141 + typeAnnotation: isRegistered ? refName : `'${refName}'`, 142 + }; 143 + }, 144 + string(schema) { 145 + return { fieldConstraints: {}, meta: defaultMeta(schema), typeAnnotation: 'str' }; 146 + }, 147 + // @ts-expect-error 148 + tuple(schema, ctx) { 149 + return { 150 + fieldConstraints: {}, 151 + meta: defaultMeta(schema), 152 + typeAnnotation: ctx.plugin.external('typing.Any'), 153 + }; 154 + }, 155 + undefined(schema) { 156 + return { 157 + fieldConstraints: {}, 158 + meta: { ...defaultMeta(schema), nullable: false, readonly: false }, 159 + typeAnnotation: 'None', 160 + }; 161 + }, 162 + // @ts-expect-error 163 + union(items, schemas, parentSchema, ctx) { 164 + return { 165 + fieldConstraints: {}, 166 + meta: composeMeta(items, { default: parentSchema.default }), 167 + typeAnnotation: ctx.plugin.external('typing.Any'), 168 + }; 169 + }, 170 + // @ts-expect-error 171 + unknown(schema, ctx) { 172 + return { 173 + fieldConstraints: {}, 174 + meta: { ...defaultMeta(schema), nullable: false, readonly: false }, 175 + typeAnnotation: ctx.plugin.external('typing.Any'), 176 + }; 177 + }, 178 + void(schema) { 179 + return { 180 + fieldConstraints: {}, 181 + meta: { ...defaultMeta(schema), nullable: false, readonly: false }, 182 + typeAnnotation: 'None', 183 + }; 184 + }, 185 + }; 186 + }
+1 -1
packages/openapi-python/src/py-dsl/decl/class.ts
··· 97 97 98 98 return py.factory.createClassDeclaration( 99 99 this.name.toString(), 100 - this.$node(this.body) as ReadonlyArray<py.Statement>, 100 + this.$node(this.body), 101 101 this.$decorators(), 102 102 this.baseClasses.map((c) => this.$node(c)), 103 103 this.$docs(),
+1 -1
packages/openapi-python/src/py-dsl/decl/func.ts
··· 67 67 return py.factory.createFunctionDeclaration( 68 68 this.name.toString(), 69 69 this._parameters, 70 - this._returnType ? (this.$node(this._returnType) as py.Expression) : undefined, 70 + this.$node(this._returnType), 71 71 this.$do(), 72 72 this.$decorators(), 73 73 this.$docs(),
+64
packages/openapi-python/src/py-dsl/expr/attr.ts
··· 1 + import type { AnalysisContext, NodeName, Ref } from '@hey-api/codegen-core'; 2 + import { fromRef, isSymbol, ref } from '@hey-api/codegen-core'; 3 + 4 + import { py } from '../../ts-python'; 5 + import type { MaybePyDsl } from '../base'; 6 + import { PyDsl } from '../base'; 7 + import { ExprMixin } from '../mixins/expr'; 8 + import { f } from '../utils/factories'; 9 + 10 + export type AttrLeft = NodeName | MaybePyDsl<py.Expression>; 11 + export type AttrCtor = (left: AttrLeft, right: NodeName) => AttrPyDsl; 12 + 13 + const Mixed = ExprMixin(PyDsl<py.MemberExpression>); 14 + 15 + export class AttrPyDsl extends Mixed { 16 + readonly '~dsl' = 'AttrPyDsl'; 17 + 18 + protected left: Ref<AttrLeft>; 19 + 20 + constructor(left: AttrLeft, right: NodeName) { 21 + super(); 22 + this.left = ref(left); 23 + this.name.set(right); 24 + } 25 + 26 + override analyze(ctx: AnalysisContext): void { 27 + super.analyze(ctx); 28 + ctx.analyze(this.left); 29 + ctx.analyze(this.name); 30 + } 31 + 32 + /** Returns true when all required builder calls are present. */ 33 + get isValid(): boolean { 34 + return this.missingRequiredCalls().length === 0; 35 + } 36 + 37 + override toAst(): py.MemberExpression { 38 + this.$validate(); 39 + 40 + const leftNode = this.$node(this.left); 41 + const right = fromRef(this.name); 42 + const value = isSymbol(right) ? right.finalName : right; 43 + return py.factory.createMemberExpression( 44 + leftNode, 45 + py.factory.createIdentifier(value as string), 46 + ); 47 + } 48 + 49 + $validate(): asserts this is this & { 50 + left: MaybePyDsl<py.Expression>; 51 + } { 52 + const missing = this.missingRequiredCalls(); 53 + if (missing.length === 0) return; 54 + throw new Error(`Attribute access missing ${missing.join(' and ')}`); 55 + } 56 + 57 + private missingRequiredCalls(): ReadonlyArray<string> { 58 + const missing: Array<string> = []; 59 + if (!this.name.toString()) missing.push('property name'); 60 + return missing; 61 + } 62 + } 63 + 64 + f.attr.set((...args) => new AttrPyDsl(...args));
+2 -2
packages/openapi-python/src/py-dsl/expr/binary.ts
··· 136 136 this.$validate(); 137 137 138 138 return py.factory.createBinaryExpression( 139 - this.$node(this._left!) as py.Expression, 139 + this.$node(this._left!), 140 140 this._op!, 141 - this.$node(this._right!) as py.Expression, 141 + this.$node(this._right!), 142 142 ); 143 143 } 144 144
+16 -26
packages/openapi-python/src/py-dsl/expr/call.ts
··· 1 - import type { AnalysisContext } from '@hey-api/codegen-core'; 1 + import type { AnalysisContext, NodeName, Ref } from '@hey-api/codegen-core'; 2 + import { ref } from '@hey-api/codegen-core'; 2 3 3 4 import { py } from '../../ts-python'; 4 5 import type { MaybePyDsl } from '../base'; 5 6 import { PyDsl } from '../base'; 7 + import { ArgsMixin } from '../mixins/args'; 8 + import { ExprMixin } from '../mixins/expr'; 9 + import { f } from '../utils/factories'; 6 10 7 - export type CallArgs = ReadonlyArray<MaybePyDsl<py.Expression> | undefined>; 11 + export type CallArgs = ReadonlyArray<CallCallee | undefined>; 12 + export type CallCallee = NodeName | MaybePyDsl<py.Expression>; 13 + export type CallCtor = (callee: CallCallee, ...args: CallArgs) => CallPyDsl; 8 14 9 - const Mixed = PyDsl<py.CallExpression>; 15 + const Mixed = ArgsMixin(ExprMixin(PyDsl<py.CallExpression>)); 10 16 11 17 export class CallPyDsl extends Mixed { 12 18 readonly '~dsl' = 'CallPyDsl'; 13 19 14 - protected _args: Array<MaybePyDsl<py.Expression> | undefined> = []; 15 - protected _callee?: MaybePyDsl<py.Expression>; 20 + protected _callee: Ref<CallCallee>; 16 21 17 - constructor(callee: MaybePyDsl<py.Expression>, ...args: CallArgs) { 22 + constructor(callee: CallCallee, ...args: CallArgs) { 18 23 super(); 19 - this._callee = callee; 20 - this._args = [...args]; 24 + this._callee = ref(callee); 25 + this.args(...args); 21 26 } 22 27 23 28 override analyze(ctx: AnalysisContext): void { 24 29 super.analyze(ctx); 25 30 ctx.analyze(this._callee); 26 - for (const arg of this._args) { 27 - if (arg) ctx.analyze(arg); 28 - } 29 31 } 30 32 31 33 /** Returns true when all required builder calls are present. */ ··· 33 35 return this.missingRequiredCalls().length === 0; 34 36 } 35 37 36 - arg(arg: MaybePyDsl<py.Expression>): this { 37 - this._args.push(arg); 38 - return this; 39 - } 40 - 41 - args(...args: CallArgs): this { 42 - this._args.push(...args); 43 - return this; 44 - } 45 - 46 38 override toAst(): py.CallExpression { 47 39 this.$validate(); 48 40 49 - const astArgs = this._args 50 - .filter((a): a is MaybePyDsl<py.Expression> => a !== undefined) 51 - .map((arg) => this.$node(arg) as py.Expression); 52 - 53 - return py.factory.createCallExpression(this.$node(this._callee!) as py.Expression, astArgs); 41 + return py.factory.createCallExpression(this.$node(this._callee!), this.$args()); 54 42 } 55 43 56 44 $validate(): asserts this is this & { ··· 67 55 return missing; 68 56 } 69 57 } 58 + 59 + f.call.set((...args) => new CallPyDsl(...args));
+2 -2
packages/openapi-python/src/py-dsl/expr/dict.ts
··· 44 44 45 45 override toAst(): py.DictExpression { 46 46 const astEntries = this._entries.map((entry) => ({ 47 - key: this.$node(entry.key) as py.Expression, 48 - value: this.$node(entry.value) as py.Expression, 47 + key: this.$node(entry.key), 48 + value: this.$node(entry.value), 49 49 })); 50 50 return py.factory.createDictExpression(astEntries); 51 51 }
+2 -6
packages/openapi-python/src/py-dsl/expr/expr.ts
··· 4 4 import type { py } from '../../ts-python'; 5 5 import type { MaybePyDsl } from '../base'; 6 6 import { PyDsl } from '../base'; 7 - // import { AsMixin } from '../mixins/as'; 8 - // import { ExprMixin } from '../mixins/expr'; 9 - // import { OperatorMixin } from '../mixins/operator'; 10 - // import { TypeExprMixin } from '../mixins/type-expr'; 7 + import { ExprMixin } from '../mixins/expr'; 11 8 12 9 type Id = NodeName | MaybePyDsl<py.Expression>; 13 10 14 - const Mixed = PyDsl<py.Expression>; 15 - // const Mixed = AsMixin(ExprMixin(OperatorMixin(TypeExprMixin(PyDsl<PyExpression>)))); 11 + const Mixed = ExprMixin(PyDsl<py.Expression>); 16 12 17 13 export class ExprPyDsl extends Mixed { 18 14 readonly '~dsl' = 'ExprPyDsl';
+1 -1
packages/openapi-python/src/py-dsl/expr/list.ts
··· 35 35 } 36 36 37 37 override toAst(): py.ListExpression { 38 - const astElements = this._elements.map((el) => this.$node(el) as py.Expression); 38 + const astElements = this._elements.map((el) => this.$node(el)); 39 39 return py.factory.createListExpression(astElements); 40 40 } 41 41 }
-59
packages/openapi-python/src/py-dsl/expr/member.ts
··· 1 - import type { AnalysisContext, NodeName } from '@hey-api/codegen-core'; 2 - 3 - import { py } from '../../ts-python'; 4 - import type { MaybePyDsl } from '../base'; 5 - import { PyDsl } from '../base'; 6 - 7 - const Mixed = PyDsl<py.MemberExpression>; 8 - 9 - export class AttrPyDsl extends Mixed { 10 - readonly '~dsl' = 'AttrPyDsl'; 11 - 12 - protected object?: MaybePyDsl<py.Expression>; 13 - protected prop?: NodeName; 14 - 15 - constructor(object: MaybePyDsl<py.Expression>, prop: NodeName) { 16 - super(); 17 - this.object = object; 18 - this.name.set(prop); 19 - } 20 - 21 - static attr(object: MaybePyDsl<py.Expression>, prop: NodeName): AttrPyDsl { 22 - return new AttrPyDsl(object, prop); 23 - } 24 - 25 - override analyze(ctx: AnalysisContext): void { 26 - super.analyze(ctx); 27 - ctx.analyze(this.object); 28 - ctx.analyze(this.name); 29 - } 30 - 31 - /** Returns true when all required builder calls are present. */ 32 - get isValid(): boolean { 33 - return this.missingRequiredCalls().length === 0; 34 - } 35 - 36 - override toAst(): py.MemberExpression { 37 - this.$validate(); 38 - 39 - return py.factory.createMemberExpression( 40 - this.$node(this.object!) as py.Expression, 41 - py.factory.createIdentifier(this.name.toString()), 42 - ); 43 - } 44 - 45 - $validate(): asserts this is this & { 46 - object: MaybePyDsl<py.Expression>; 47 - } { 48 - const missing = this.missingRequiredCalls(); 49 - if (missing.length === 0) return; 50 - throw new Error(`Attribute access missing ${missing.join(' and ')}`); 51 - } 52 - 53 - private missingRequiredCalls(): ReadonlyArray<string> { 54 - const missing: Array<string> = []; 55 - if (!this.object) missing.push('object'); 56 - if (!this.name.toString()) missing.push('property name'); 57 - return missing; 58 - } 59 - }
+1 -1
packages/openapi-python/src/py-dsl/expr/set.ts
··· 35 35 } 36 36 37 37 override toAst(): py.SetExpression { 38 - const astElements = this._elements.map((el) => this.$node(el) as py.Expression); 38 + const astElements = this._elements.map((el) => this.$node(el)); 39 39 return py.factory.createSetExpression(astElements); 40 40 } 41 41 }
+46
packages/openapi-python/src/py-dsl/expr/subscript.ts
··· 1 + import type { AnalysisContext, NodeName } from '@hey-api/codegen-core'; 2 + 3 + import { py } from '../../ts-python'; 4 + import type { MaybePyDsl } from '../base'; 5 + import { PyDsl } from '../base'; 6 + import { LayoutMixin } from '../mixins/layout'; 7 + import { f } from '../utils/factories'; 8 + 9 + export type SubscriptExpr = NodeName | MaybePyDsl<py.Expression>; 10 + export type SubscriptCtor = ( 11 + value: SubscriptExpr, 12 + ...slices: Array<SubscriptExpr> 13 + ) => SubscriptPyDsl; 14 + 15 + const Mixed = LayoutMixin(PyDsl<py.SubscriptExpression>); 16 + 17 + export class SubscriptPyDsl extends Mixed { 18 + readonly '~dsl' = 'SubscriptPyDsl'; 19 + 20 + protected _slices: Array<SubscriptExpr>; 21 + protected _value: SubscriptExpr; 22 + 23 + constructor(value: SubscriptExpr, ...slices: Array<SubscriptExpr>) { 24 + super(); 25 + this._slices = slices; 26 + this._value = value; 27 + } 28 + 29 + override analyze(ctx: AnalysisContext): void { 30 + super.analyze(ctx); 31 + ctx.analyze(this._value); 32 + for (const slice of this._slices) { 33 + ctx.analyze(slice); 34 + } 35 + } 36 + 37 + override toAst(): py.SubscriptExpression { 38 + const slice = 39 + this._slices.length === 1 40 + ? this.$node(this._slices[0]!) 41 + : py.factory.createSubscriptSlice(this._slices.map((s) => this.$node(s))); 42 + return py.factory.createSubscriptExpression(this.$node(this._value), slice); 43 + } 44 + } 45 + 46 + f.slice.set((...args) => new SubscriptPyDsl(...args));
+8 -6
packages/openapi-python/src/py-dsl/expr/tuple.ts
··· 1 - import type { AnalysisContext } from '@hey-api/codegen-core'; 1 + import type { AnalysisContext, NodeName } from '@hey-api/codegen-core'; 2 2 3 3 import { py } from '../../ts-python'; 4 4 import type { MaybePyDsl } from '../base'; 5 5 import { PyDsl } from '../base'; 6 6 import { LayoutMixin } from '../mixins/layout'; 7 + 8 + export type TupleElement = NodeName | MaybePyDsl<py.Expression>; 7 9 8 10 const Mixed = LayoutMixin(PyDsl<py.TupleExpression>); 9 11 10 12 export class TuplePyDsl extends Mixed { 11 13 readonly '~dsl' = 'TuplePyDsl'; 12 14 13 - protected _elements: Array<MaybePyDsl<py.Expression>> = []; 15 + protected _elements: Array<TupleElement> = []; 14 16 15 - constructor(...elements: Array<MaybePyDsl<py.Expression>>) { 17 + constructor(...elements: Array<TupleElement>) { 16 18 super(); 17 19 this._elements = elements; 18 20 } ··· 24 26 } 25 27 } 26 28 27 - element(expr: MaybePyDsl<py.Expression>): this { 29 + element(expr: TupleElement): this { 28 30 this._elements.push(expr); 29 31 return this; 30 32 } 31 33 32 - elements(...exprs: ReadonlyArray<MaybePyDsl<py.Expression>>): this { 34 + elements(...exprs: ReadonlyArray<TupleElement>): this { 33 35 this._elements.push(...exprs); 34 36 return this; 35 37 } 36 38 37 39 override toAst(): py.TupleExpression { 38 - const astElements = this._elements.map((el) => this.$node(el) as py.Expression); 40 + const astElements = this._elements.map((el) => this.$node(el)); 39 41 return py.factory.createTupleExpression(astElements); 40 42 } 41 43 }
+5 -1
packages/openapi-python/src/py-dsl/index.ts
··· 6 6 // import { EnumPyDsl } from './decl/enum'; 7 7 // import { FieldPyDsl } from './decl/field'; 8 8 import { FuncPyDsl } from './decl/func'; 9 + import { AttrPyDsl } from './expr/attr'; 9 10 // import { GetterPyDsl } from './decl/getter'; 10 11 // import { InitPyDsl } from './decl/init'; 11 12 // import { EnumMemberPyDsl } from './decl/member'; ··· 24 25 import { IdPyDsl } from './expr/identifier'; 25 26 import { ListPyDsl } from './expr/list'; 26 27 import { LiteralPyDsl } from './expr/literal'; 27 - import { AttrPyDsl } from './expr/member'; 28 28 // import { NewPyDsl } from './expr/new'; 29 29 // import { ObjectPyDsl } from './expr/object'; 30 30 // import { PrefixPyDsl } from './expr/prefix'; 31 31 // import { ObjectPropPyDsl } from './expr/prop'; 32 32 // import { RegExpPyDsl } from './expr/regexp'; 33 33 import { SetPyDsl } from './expr/set'; 34 + import { SubscriptPyDsl } from './expr/subscript'; 34 35 // import { TemplatePyDsl } from './expr/template'; 35 36 // import { TernaryPyDsl } from './expr/ternary'; 36 37 import { TuplePyDsl } from './expr/tuple'; ··· 208 209 209 210 /** Wraps an expression or statement-like value into a `StmtPyDsl`. */ 210 211 stmt: (...args: ConstructorParameters<typeof StmtPyDsl>) => new StmtPyDsl(...args), 212 + 213 + /** Creates a subscript expression (e.g. `obj[index]` or `Type[Param]`). */ 214 + subscript: (...args: ConstructorParameters<typeof SubscriptPyDsl>) => new SubscriptPyDsl(...args), 211 215 212 216 /** Creates a template literal expression. */ 213 217 // template: (...args: ConstructorParameters<typeof TemplateTsDsl>) => new TemplateTsDsl(...args),
+8 -4
packages/openapi-python/src/py-dsl/mixins/args.ts
··· 8 8 type Arg = NodeName | MaybePyDsl<py.Expression>; 9 9 10 10 export interface ArgsMethods extends Node { 11 + /** Renders the arguments into an array of `Expression`s. */ 11 12 $args(): ReadonlyArray<py.Expression>; 13 + /** Adds a single expression argument. */ 12 14 arg(arg: Arg | undefined): this; 15 + /** Adds one or more expression arguments. */ 13 16 args(...args: ReadonlyArray<Arg | undefined>): this; 14 17 } 15 18 19 + /** 20 + * Adds `.arg()` and `.args()` for managing expression arguments in call-like nodes. 21 + */ 16 22 export function ArgsMixin<T extends py.Node, TBase extends BaseCtor<T>>(Base: TBase) { 17 23 abstract class Args extends Base { 18 24 protected _args: Array<Ref<Arg>> = []; ··· 31 37 32 38 protected args(...args: ReadonlyArray<Arg | undefined>): this { 33 39 this._args.push( 34 - ...args 35 - .filter((a): a is NonNullable<typeof a> => a !== undefined) 36 - .map((a) => ref(a as Arg)), 40 + ...args.filter((a): a is NonNullable<typeof a> => a !== undefined).map((a) => ref(a)), 37 41 ); 38 42 return this; 39 43 } 40 44 41 45 protected $args(): ReadonlyArray<py.Expression> { 42 - return this.$node(this._args).map((arg) => this.$node(arg) as py.Expression); 46 + return this.$node(this._args).map((arg) => this.$node(arg)); 43 47 } 44 48 } 45 49
+1 -1
packages/openapi-python/src/py-dsl/mixins/decorator.ts
··· 34 34 const decoratorExpr = args.length 35 35 ? py.factory.createCallExpression( 36 36 nameNode as py.Expression, 37 - args.map((a) => this.$node(a) as py.Expression), 37 + args.map((a) => this.$node(a)), 38 38 ) 39 39 : (nameNode as py.Expression); 40 40 this.decorators.push(decoratorExpr);
+26 -13
packages/openapi-python/src/py-dsl/mixins/expr.ts
··· 1 1 import type { AnalysisContext, Node } from '@hey-api/codegen-core'; 2 2 3 3 import type { py } from '../../ts-python'; 4 - import type { MaybePyDsl } from '../base'; 5 - import { CallPyDsl } from '../expr/call'; 6 - import { AttrPyDsl } from '../expr/member'; 7 - import type { BaseCtor, MixinCtor } from './types'; 4 + import { f } from '../utils/factories'; 5 + import type { BaseCtor, DropFirst, MixinCtor } from './types'; 8 6 9 7 export interface ExprMethods extends Node { 10 - attr(object: MaybePyDsl<py.Expression>, prop: string): AttrPyDsl; 11 - call(callee: MaybePyDsl<py.Expression>, ...args: Array<MaybePyDsl<py.Expression>>): CallPyDsl; 8 + /** Accesses a property on the current expression (e.g. `this.foo`). */ 9 + attr(...args: DropFirst<Parameters<typeof f.attr>>): ReturnType<typeof f.attr>; 10 + /** Calls the current expression (e.g. `fn(arg1, arg2)`). */ 11 + call(...args: DropFirst<Parameters<typeof f.call>>): ReturnType<typeof f.call>; 12 + /** Produces a `return` statement returning the current expression. */ 13 + return(): ReturnType<typeof f.return>; 14 + /** Produces a subscript/slice expression (e.g. `expr[args]`). */ 15 + slice(...args: DropFirst<Parameters<typeof f.slice>>): ReturnType<typeof f.slice>; 12 16 } 13 17 14 18 export function ExprMixin<T extends py.Expression, TBase extends BaseCtor<T>>(Base: TBase) { ··· 17 21 super.analyze(ctx); 18 22 } 19 23 20 - protected attr(object: MaybePyDsl<py.Expression>, prop: string): AttrPyDsl { 21 - return AttrPyDsl.attr(object, prop); 24 + protected attr(...args: DropFirst<Parameters<typeof f.attr>>): ReturnType<typeof f.attr> { 25 + // @ts-expect-error - fix this type 26 + return f.attr(this, ...args); 27 + } 28 + 29 + protected call(...args: DropFirst<Parameters<typeof f.call>>): ReturnType<typeof f.call> { 30 + // @ts-expect-error - fix this type 31 + return f.call(this, ...args); 32 + } 33 + 34 + protected return(): ReturnType<typeof f.return> { 35 + // @ts-expect-error - fix this type 36 + return f.return(this); 22 37 } 23 38 24 - protected call( 25 - callee: MaybePyDsl<py.Expression>, 26 - ...args: Array<MaybePyDsl<py.Expression>> 27 - ): CallPyDsl { 28 - return new CallPyDsl(callee, ...args); 39 + protected slice(...args: DropFirst<Parameters<typeof f.slice>>): ReturnType<typeof f.slice> { 40 + // @ts-expect-error - fix this type 41 + return f.slice(this, ...args); 29 42 } 30 43 } 31 44
+2 -2
packages/openapi-python/src/py-dsl/stmt/for.ts
··· 74 74 const elseBlock = this._else ? new BlockPyDsl(...this._else).$do() : undefined; 75 75 76 76 return py.factory.createForStatement( 77 - this.$node(this._target!) as py.Expression, 78 - this.$node(this._iterable!) as py.Expression, 77 + this.$node(this._target!), 78 + this.$node(this._iterable!), 79 79 [...body], 80 80 elseBlock ? [...elseBlock] : undefined, 81 81 );
+2 -2
packages/openapi-python/src/py-dsl/stmt/return.ts
··· 4 4 import { py } from '../../ts-python'; 5 5 import type { MaybePyDsl } from '../base'; 6 6 import { PyDsl } from '../base'; 7 - // import { f } from '../utils/factories'; 7 + import { f } from '../utils/factories'; 8 8 9 9 export type ReturnExpr = NodeName | MaybePyDsl<py.Expression>; 10 10 export type ReturnCtor = (expr?: ReturnExpr) => ReturnPyDsl; ··· 31 31 } 32 32 } 33 33 34 - // f.return.set((...args) => new ReturnPyDsl(...args)); 34 + f.return.set((...args) => new ReturnPyDsl(...args));
+1 -1
packages/openapi-python/src/py-dsl/stmt/stmt.ts
··· 23 23 override toAst() { 24 24 const node = this.$node(this._inner); 25 25 if (isStatement(node)) return node; 26 - return py.factory.createExpressionStatement(node as py.Expression); 26 + return py.factory.createExpressionStatement(node); 27 27 } 28 28 } 29 29
+1 -3
packages/openapi-python/src/py-dsl/stmt/try.ts
··· 172 172 if (entry.types.length === 1) { 173 173 exceptionType = this.$node(entry.types[0]!); 174 174 } else if (entry.types.length > 1) { 175 - exceptionType = py.factory.createTupleExpression( 176 - entry.types.map((t) => this.$node(t) as py.Expression), 177 - ); 175 + exceptionType = py.factory.createTupleExpression(entry.types.map((t) => this.$node(t))); 178 176 } 179 177 180 178 const exceptionName = entry.name
+1 -1
packages/openapi-python/src/py-dsl/stmt/while.ts
··· 67 67 const elseBlock = this._else ? new BlockPyDsl(...this._else).$do() : undefined; 68 68 69 69 return py.factory.createWhileStatement( 70 - this.$node(this._condition!) as py.Expression, 70 + this.$node(this._condition!), 71 71 [...body], 72 72 elseBlock ? [...elseBlock] : undefined, 73 73 );
+3 -6
packages/openapi-python/src/py-dsl/stmt/with.ts
··· 77 77 78 78 const astItems = this._items.map((item) => { 79 79 if (typeof item === 'object' && 'context' in item) { 80 - return py.factory.createWithItem( 81 - this.$node(item.context) as py.Expression, 82 - item.alias ? (this.$node(item.alias) as py.Expression) : undefined, 83 - ); 80 + return py.factory.createWithItem(this.$node(item.context), this.$node(item.alias)); 84 81 } 85 - return py.factory.createWithItem(this.$node(item) as py.Expression, undefined); 82 + return py.factory.createWithItem(this.$node(item), undefined); 86 83 }); 87 84 88 85 const body = new BlockPyDsl(...this._body!).$do(); ··· 90 87 return py.factory.createWithStatement( 91 88 astItems, 92 89 [...body], 93 - this._modifier ? [this.$node(this._modifier) as py.Expression] : undefined, 90 + this._modifier ? [this.$node(this._modifier)] : undefined, 94 91 ); 95 92 } 96 93
+41
packages/openapi-python/src/py-dsl/utils/factories.ts
··· 1 + import type { AttrCtor } from '../expr/attr'; 2 + import type { CallCtor } from '../expr/call'; 3 + import type { SubscriptCtor } from '../expr/subscript'; 4 + import type { ReturnCtor } from '../stmt/return'; 5 + 6 + type Ctor = (...args: Array<any>) => any; 7 + 8 + type Factory<T extends Ctor> = { 9 + (...args: Parameters<T>): ReturnType<T>; 10 + /** Sets the implementation of this factory. */ 11 + set(fn: T): void; 12 + }; 13 + 14 + function createFactory<T extends Ctor>(name: string): Factory<T> { 15 + let impl: T | undefined; 16 + 17 + const slot = ((...args: Parameters<T>) => { 18 + if (!impl) throw new Error(`${name} factory not registered`); 19 + return impl(...args); 20 + }) as Factory<T>; 21 + 22 + slot.set = (fn: T) => { 23 + impl = fn; 24 + }; 25 + 26 + return slot; 27 + } 28 + 29 + export const f = { 30 + /** Factory for creating property access expressions (e.g. `obj.foo`). */ 31 + attr: createFactory<AttrCtor>('attr'), 32 + 33 + /** Factory for creating function or method call expressions (e.g. `fn(arg)`). */ 34 + call: createFactory<CallCtor>('call'), 35 + 36 + /** Factory for creating return statements. */ 37 + return: createFactory<ReturnCtor>('return'), 38 + 39 + /** Factory for creating slice expressions. */ 40 + slice: createFactory<SubscriptCtor>('slice'), 41 + };
packages/openapi-python/src/ts-python/__snapshots__/nodes/expressions/subscript/__init__.py

This is a binary file and will not be displayed.

+2
packages/openapi-python/src/ts-python/__snapshots__/nodes/expressions/subscript/index-access.py
··· 1 + items = (1, 2, 3) 2 + first = items[0]
+1
packages/openapi-python/src/ts-python/__snapshots__/nodes/expressions/subscript/multiple.py
··· 1 + data = dict[str, int]
+1
packages/openapi-python/src/ts-python/__snapshots__/nodes/expressions/subscript/nested.py
··· 1 + matrix = list[list[int]]
+1
packages/openapi-python/src/ts-python/__snapshots__/nodes/expressions/subscript/single.py
··· 1 + numbers = list[int]
+70
packages/openapi-python/src/ts-python/__tests__/nodes/expressions/subscript.test.ts
··· 1 + import { py } from '../../../index'; 2 + import { assertPrintedMatchesSnapshot } from '../utils'; 3 + 4 + describe('subscript expression', () => { 5 + it('type annotation with single type parameter', async () => { 6 + const file = py.factory.createSourceFile([ 7 + py.factory.createAssignment( 8 + py.factory.createIdentifier('numbers'), 9 + py.factory.createSubscriptExpression( 10 + py.factory.createIdentifier('list'), 11 + py.factory.createIdentifier('int'), 12 + ), 13 + ), 14 + ]); 15 + await assertPrintedMatchesSnapshot(file, 'single.py'); 16 + }); 17 + 18 + it('type annotation with multiple type parameters', async () => { 19 + const file = py.factory.createSourceFile([ 20 + py.factory.createAssignment( 21 + py.factory.createIdentifier('data'), 22 + py.factory.createSubscriptExpression( 23 + py.factory.createIdentifier('dict'), 24 + py.factory.createSubscriptSlice([ 25 + py.factory.createIdentifier('str'), 26 + py.factory.createIdentifier('int'), 27 + ]), 28 + ), 29 + ), 30 + ]); 31 + await assertPrintedMatchesSnapshot(file, 'multiple.py'); 32 + }); 33 + 34 + it('index access', async () => { 35 + const file = py.factory.createSourceFile([ 36 + py.factory.createAssignment( 37 + py.factory.createIdentifier('items'), 38 + py.factory.createTupleExpression([ 39 + py.factory.createLiteral(1), 40 + py.factory.createLiteral(2), 41 + py.factory.createLiteral(3), 42 + ]), 43 + ), 44 + py.factory.createAssignment( 45 + py.factory.createIdentifier('first'), 46 + py.factory.createSubscriptExpression( 47 + py.factory.createIdentifier('items'), 48 + py.factory.createLiteral(0), 49 + ), 50 + ), 51 + ]); 52 + await assertPrintedMatchesSnapshot(file, 'index-access.py'); 53 + }); 54 + 55 + it('nested type annotation', async () => { 56 + const file = py.factory.createSourceFile([ 57 + py.factory.createAssignment( 58 + py.factory.createIdentifier('matrix'), 59 + py.factory.createSubscriptExpression( 60 + py.factory.createIdentifier('list'), 61 + py.factory.createSubscriptExpression( 62 + py.factory.createIdentifier('list'), 63 + py.factory.createIdentifier('int'), 64 + ), 65 + ), 66 + ), 67 + ]); 68 + await assertPrintedMatchesSnapshot(file, 'nested.py'); 69 + }); 70 + });
+4
packages/openapi-python/src/ts-python/index.ts
··· 26 26 import type { PyLiteral as _PyLiteral } from './nodes/expressions/literal'; 27 27 import type { PyMemberExpression as _PyMemberExpression } from './nodes/expressions/member'; 28 28 import type { PySetExpression as _PySetExpression } from './nodes/expressions/set'; 29 + import type { PySubscriptExpression as _PySubscriptExpression } from './nodes/expressions/subscript'; 30 + import type { PySubscriptSlice as _PySubscriptSlice } from './nodes/expressions/subscript-slice'; 29 31 import type { PyTupleExpression as _PyTupleExpression } from './nodes/expressions/tuple'; 30 32 import type { PyYieldExpression as _PyYieldExpression } from './nodes/expressions/yield'; 31 33 import type { PyYieldFromExpression as _PyYieldFromExpression } from './nodes/expressions/yieldFrom'; ··· 110 112 export type Literal = _PyLiteral; 111 113 export type MemberExpression = _PyMemberExpression; 112 114 export type SetExpression = _PySetExpression; 115 + export type SubscriptExpression = _PySubscriptExpression; 116 + export type SubscriptSlice = _PySubscriptSlice; 113 117 export type TupleExpression = _PyTupleExpression; 114 118 export type YieldExpression = _PyYieldExpression; 115 119 export type YieldFromExpression = _PyYieldFromExpression;
+4
packages/openapi-python/src/ts-python/nodes/expression.ts
··· 12 12 import type { PyLiteral } from './expressions/literal'; 13 13 import type { PyMemberExpression } from './expressions/member'; 14 14 import type { PySetExpression } from './expressions/set'; 15 + import type { PySubscriptExpression } from './expressions/subscript'; 16 + import type { PySubscriptSlice } from './expressions/subscript-slice'; 15 17 import type { PyTupleExpression } from './expressions/tuple'; 16 18 import type { PyYieldExpression } from './expressions/yield'; 17 19 import type { PyYieldFromExpression } from './expressions/yieldFrom'; ··· 31 33 | PyLiteral 32 34 | PyMemberExpression 33 35 | PySetExpression 36 + | PySubscriptExpression 37 + | PySubscriptSlice 34 38 | PyTupleExpression 35 39 | PyYieldExpression 36 40 | PyYieldFromExpression;
+15
packages/openapi-python/src/ts-python/nodes/expressions/subscript-slice.ts
··· 1 + import type { PyNodeBase } from '../base'; 2 + import type { PyExpression } from '../expression'; 3 + import { PyNodeKind } from '../kinds'; 4 + 5 + export interface PySubscriptSlice extends PyNodeBase { 6 + elements: ReadonlyArray<PyExpression>; 7 + kind: PyNodeKind.SubscriptSlice; 8 + } 9 + 10 + export function createSubscriptSlice(elements: ReadonlyArray<PyExpression>): PySubscriptSlice { 11 + return { 12 + elements, 13 + kind: PyNodeKind.SubscriptSlice, 14 + }; 15 + }
+20
packages/openapi-python/src/ts-python/nodes/expressions/subscript.ts
··· 1 + import type { PyNodeBase } from '../base'; 2 + import type { PyExpression } from '../expression'; 3 + import { PyNodeKind } from '../kinds'; 4 + 5 + export interface PySubscriptExpression extends PyNodeBase { 6 + kind: PyNodeKind.SubscriptExpression; 7 + slice: PyExpression; 8 + value: PyExpression; 9 + } 10 + 11 + export function createSubscriptExpression( 12 + value: PyExpression, 13 + slice: PyExpression, 14 + ): PySubscriptExpression { 15 + return { 16 + kind: PyNodeKind.SubscriptExpression, 17 + slice, 18 + value, 19 + }; 20 + }
+4
packages/openapi-python/src/ts-python/nodes/factory.ts
··· 17 17 import { createLiteral } from './expressions/literal'; 18 18 import { createMemberExpression } from './expressions/member'; 19 19 import { createSetExpression } from './expressions/set'; 20 + import { createSubscriptExpression } from './expressions/subscript'; 21 + import { createSubscriptSlice } from './expressions/subscript-slice'; 20 22 import { createTupleExpression } from './expressions/tuple'; 21 23 import { createYieldExpression } from './expressions/yield'; 22 24 import { createYieldFromExpression } from './expressions/yieldFrom'; ··· 75 77 createSetComprehension, 76 78 createSetExpression, 77 79 createSourceFile, 80 + createSubscriptExpression, 81 + createSubscriptSlice, 78 82 createTryStatement, 79 83 createTupleExpression, 80 84 createWhileStatement,
+2
packages/openapi-python/src/ts-python/nodes/kinds.ts
··· 33 33 SetComprehension = 'SetComprehension', 34 34 SetExpression = 'SetExpression', 35 35 SourceFile = 'SourceFile', 36 + SubscriptExpression = 'SubscriptExpression', 37 + SubscriptSlice = 'SubscriptSlice', 36 38 TryStatement = 'TryStatement', 37 39 TupleExpression = 'TupleExpression', 38 40 WhileStatement = 'WhileStatement',
+8
packages/openapi-python/src/ts-python/printer.ts
··· 328 328 parts.push(...node.statements.map(printNode)); 329 329 break; 330 330 331 + case PyNodeKind.SubscriptExpression: 332 + parts.push(`${printNode(node.value)}[${printNode(node.slice)}]`); 333 + break; 334 + 335 + case PyNodeKind.SubscriptSlice: 336 + parts.push(node.elements.map(printNode).join(', ')); 337 + break; 338 + 331 339 case PyNodeKind.TryStatement: { 332 340 parts.push(printLine('try:'), printNode(node.tryBlock)); 333 341 if (node.exceptClauses) {
-2
packages/openapi-ts/src/plugins/valibot/shared/export.ts
··· 2 2 3 3 import { createSchemaComment } from '../../../plugins/shared/utils/schema'; 4 4 import { $ } from '../../../ts-dsl'; 5 - import type { ValibotPlugin } from '../types'; 6 5 import { pipesToNode } from './pipes'; 7 6 import type { ProcessorContext } from './processor'; 8 7 import type { ValibotFinal } from './types'; ··· 18 17 tags, 19 18 }: ProcessorContext & { 20 19 final: ValibotFinal; 21 - plugin: ValibotPlugin['Instance']; 22 20 }): void { 23 21 const v = plugin.external('valibot.v'); 24 22
+3 -7
packages/openapi-ts/src/plugins/valibot/shared/meta.ts
··· 27 27 children: ReadonlyArray<ValibotResult>, 28 28 overrides?: Partial<ValibotMeta>, 29 29 ): ValibotMeta { 30 - const hasLazy = overrides?.hasLazy ?? children.some((c) => c.meta.hasLazy); 31 - const nullable = overrides?.nullable ?? children.some((c) => c.meta.nullable); 32 - const readonly = overrides?.readonly ?? children.some((c) => c.meta.readonly); 33 - 34 30 return { 35 31 default: overrides?.default, 36 32 format: overrides?.format, 37 - hasLazy, 38 - nullable, 39 - readonly, 33 + hasLazy: overrides?.hasLazy ?? children.some((c) => c.meta.hasLazy), 34 + nullable: overrides?.nullable ?? children.some((c) => c.meta.nullable), 35 + readonly: overrides?.readonly ?? children.some((c) => c.meta.readonly), 40 36 }; 41 37 } 42 38
+1 -5
packages/openapi-ts/src/plugins/valibot/v1/processor.ts
··· 50 50 plugin, 51 51 }) as ValibotFinal; 52 52 53 - exportAst({ 54 - ...ctx, 55 - final, 56 - plugin, 57 - }); 53 + exportAst({ ...ctx, final, plugin }); 58 54 }); 59 55 } 60 56
+8 -8
packages/openapi-ts/src/ts-dsl/expr/call.ts
··· 10 10 import { TypeArgsMixin } from '../mixins/type-args'; 11 11 import { f } from '../utils/factories'; 12 12 13 - export type CallArgs = ReadonlyArray<CallExpr | undefined>; 14 - export type CallExpr = NodeName | MaybeTsDsl<ts.Expression>; 15 - export type CallCtor = (expr: CallExpr, ...args: CallArgs) => CallTsDsl; 13 + export type CallArgs = ReadonlyArray<CallCallee | undefined>; 14 + export type CallCallee = NodeName | MaybeTsDsl<ts.Expression>; 15 + export type CallCtor = (callee: CallCallee, ...args: CallArgs) => CallTsDsl; 16 16 17 17 const Mixed = ArgsMixin(AsMixin(ExprMixin(TypeArgsMixin(TsDsl<ts.CallExpression>)))); 18 18 19 19 export class CallTsDsl extends Mixed { 20 20 readonly '~dsl' = 'CallTsDsl'; 21 21 22 - protected _callExpr: Ref<CallExpr>; 22 + protected _callee: Ref<CallCallee>; 23 23 24 - constructor(expr: CallExpr, ...args: CallArgs) { 24 + constructor(callee: CallCallee, ...args: CallArgs) { 25 25 super(); 26 - this._callExpr = ref(expr); 26 + this._callee = ref(callee); 27 27 this.args(...args); 28 28 } 29 29 30 30 override analyze(ctx: AnalysisContext): void { 31 31 super.analyze(ctx); 32 - ctx.analyze(this._callExpr); 32 + ctx.analyze(this._callee); 33 33 } 34 34 35 35 override toAst() { 36 36 return ts.factory.createCallExpression( 37 - this.$node(this._callExpr), 37 + this.$node(this._callee), 38 38 this.$generics(), 39 39 this.$args(), 40 40 );