Skip to content
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
28 changes: 9 additions & 19 deletions codegen/lib/class-model.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
// Ported from @seamapi/nextlove-sdk-generator lib/generate-python-sdk/class-file.ts.
// Holds the data previously carried by the nextlove ClassFile; all string
// serialization moved to the Handlebars layouts and their context builders.
// Holds the class and method data assembled by the routes plugin; all string
// serialization lives in the Handlebars layouts and their context builders.

export interface ClassMethodParameter {
name: string
Expand All @@ -13,7 +12,7 @@ export interface ClassMethod {
methodName: string
path: string
parameters: ClassMethodParameter[]
returnPath: Array<string | undefined>
returnPath: string[]
returnResource: string
}

Expand All @@ -29,21 +28,12 @@ export interface ClassModel {
childClassIdentifiers: ChildClassIdentifier[]
}

// Verbatim port of the nextlove parameter comparator. The original
// expression `(a.position ?? a.required ? 1000 : 9999)` parses as
// `(a.position ?? a.required) ? 1000 : 9999`, so a parameter with
// position 0 is falsy and lands in the 9999 tier together with the
// optional parameters. Combined with a stable sort this yields: required
// parameters first (in schema order), then everything else (in schema
// order).
// TODO: Fix the operator precedence so position sorts a parameter first
// as originally intended, once generated output is allowed to change.
// Until then, do not "fix" it: the generated output must stay identical.
// Sort tiers: an explicit position first, then required parameters, then
// optional parameters. Array#sort is stable, so ties keep schema order.
const sortTier = ({ position, required }: ClassMethodParameter): number =>
position ?? ((required ?? false) ? 1000 : 9999)

export const sortClassMethodParameters = (
parameters: ClassMethodParameter[],
): ClassMethodParameter[] =>
[...parameters].sort(
(a, b) =>
((a.position ?? a.required) ? 1000 : 9999) -
((b.position ?? b.required) ? 1000 : 9999),
)
[...parameters].sort((a, b) => sortTier(a) - sortTier(b))
23 changes: 0 additions & 23 deletions codegen/lib/endpoint-rules.ts

This file was deleted.

85 changes: 51 additions & 34 deletions codegen/lib/layouts/models.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,19 @@
// Builds the template context for seam/routes/models.py.
// Mirrors the models.py assembly in the nextlove generate-python-sdk.ts plus
// ClassFile#serializeToAbstractClassWithoutImports.
// Dataclasses come from the blueprint resources, events, action attempts, and
// pagination; the abstract route classes mirror the generated route classes.

import type { Blueprint, Property, Resource } from '@seamapi/blueprint'
import { pascalCase } from 'change-case'

import type { ClassModel } from '../class-model.js'
import { convertCustomResourceName } from '../custom-resource-name-conversions.js'
import { mapPythonType } from '../map-python-type.js'
import { flattenObjSchema } from '../openapi/flatten-obj-schema.js'
import type { ObjSchema, OpenapiSchema } from '../openapi/types.js'
import { mapPropertyToPythonType } from '../python-type.js'
import { getMethodLayoutContext } from './route.js'

// Python hard keywords cannot be used as identifiers. When a property name
// collides with one (e.g. "from"), the dataclass field and keyword argument
// are suffixed with an underscore while the original name is preserved as the
// dict key. No existing property name is a hard keyword, so this leaves all
// other generated output unchanged.
// dict key.
const PYTHON_KEYWORDS = new Set([
'False',
'None',
Expand Down Expand Up @@ -81,37 +79,56 @@ export interface ModelsLayoutContext {
routesNamespaces: Array<{ namespace: string; abstractClassName: string }>
}

// The action attempt and event variants each generate a single dataclass with
// the union of the variant properties. The first occurrence of a property
// name wins.
const mergeResourceProperties = (resources: Resource[]): Property[] => {
const merged = new Map<string, Property>()
for (const { properties } of resources) {
for (const property of properties) {
if (!merged.has(property.name)) merged.set(property.name, property)
}
}
return [...merged.values()]
}

export const setModelsLayoutContext = (
openapi: OpenapiSchema,
blueprint: Blueprint,
classMap: Map<string, ClassModel>,
topLevelNamespaces: string[],
): ModelsLayoutContext => {
// TODO: Use blueprint.resources, blueprint.events, and
// blueprint.actionAttempts once generated output is allowed to change.
// Blueprint currently omits some schemas (e.g. pagination and
// phone_registration), reorders others, and collapses integer to number,
// so the raw OpenAPI schemas are used to keep the output identical.
const resources = Object.entries(openapi.components.schemas)
.map(
([schemaName, schema]) =>
[schemaName, flattenObjSchema(schema as ObjSchema)] as [
string,
ObjSchema,
],
)
.map(([schemaName, schema]) => ({
className: pascalCase(convertCustomResourceName(schemaName)),
properties: Object.entries(schema.properties).map(
([name, propertySchema]) => {
const type = mapPythonType(propertySchema)
return {
name,
safeName: toSafeIdentifier(name),
type,
isDictParam: type.startsWith('Dict') || name === 'properties',
}
},
),
const models = new Map<string, Property[]>()

for (const resource of blueprint.resources) {
models.set(resource.resourceType, resource.properties)
}

// The event and action attempt variants merge into a single dataclass with
// the union of the variant properties, overriding the base resource schema.
models.set(
'action_attempt',
mergeResourceProperties(blueprint.actionAttempts),
)
models.set('event', mergeResourceProperties(blueprint.events))

if (blueprint.pagination != null) {
models.set('pagination', blueprint.pagination.properties)
}

const resources = [...models.entries()]
.sort(([a], [b]) => (a < b ? -1 : 1))
.map(([name, properties]) => ({
className: pascalCase(convertCustomResourceName(name)),
properties: properties.map((property) => {
const type = mapPropertyToPythonType(property)
return {
name: property.name,
safeName: toSafeIdentifier(property.name),
type,
isDictParam:
type.startsWith('Dict') || property.name === 'properties',
}
}),
}))

const abstractClasses = [...classMap.values()]
Expand Down
3 changes: 2 additions & 1 deletion codegen/lib/layouts/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,8 @@ export const getMethodLayoutContext = (
pollsActionAttempt,
isList,
itemType: returnResourceItem,
resAccessor: `res["${returnPath.join('"]["')}"]`,
resAccessor:
returnPath.length > 0 ? `res["${returnPath.join('"]["')}"]` : '',
}
}

Expand Down
46 changes: 0 additions & 46 deletions codegen/lib/map-python-type.ts

This file was deleted.

138 changes: 0 additions & 138 deletions codegen/lib/openapi/deep-flatten-one-of-and-all-of-schema.ts

This file was deleted.

Loading
Loading