Skip to content
Draft
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
1 change: 0 additions & 1 deletion codegen/layouts/seam-client.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,6 @@ class SeamClient
];
$options = array_filter($options, fn($option) => $option !== null);

// TODO handle request errors
$response = $this->client->request($method, $path, $options);
$status_code = $response->getStatusCode();
$request_id = $response->getHeaderLine("seam-request-id");
Expand Down
26 changes: 8 additions & 18 deletions codegen/lib/class-model.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
// Ported from @seamapi/nextlove-sdk-generator
// lib/generate-php-sdk/utils/php-client.ts. Holds the data previously carried
// by the nextlove PhpClient; all string serialization moved to the Handlebars
// layouts and their context builders.
// Data model for the generated resource client classes. All string
// serialization lives in the Handlebars layouts and their context builders.

export interface PhpClientMethodParameter {
name: string
Expand Down Expand Up @@ -32,20 +30,12 @@ export interface PhpClient {
methods: PhpClientMethod[]
}

// 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.
// Sorts parameters with an explicit position first, then required parameters,
// then everything else, keeping the schema order within each tier.
export const sortPhpClientMethodParameters = (
parameters: PhpClientMethodParameter[],
): PhpClientMethodParameter[] =>
[...parameters].sort(
(a, b) =>
((a.position ?? a.required) ? 1000 : 9999) -
((b.position ?? b.required) ? 1000 : 9999),
)
[...parameters].sort((a, b) => getParameterRank(a) - getParameterRank(b))

const getParameterRank = (parameter: PhpClientMethodParameter): number =>
parameter.position ?? ((parameter.required ?? false) ? 1000 : 9999)
13 changes: 0 additions & 13 deletions codegen/lib/endpoint-rules.ts

This file was deleted.

130 changes: 40 additions & 90 deletions codegen/lib/layouts/object.ts
Original file line number Diff line number Diff line change
@@ -1,114 +1,64 @@
// Builds the template context for resource object files (src/Objects/{Name}.php).
// Mirrors the nextlove generate-resource-object-class.ts serialization: the
// from_json body lines and the constructor parameter lines, each already
// sorted with the nextlove property comparator.

import { getPhpType } from '../map-php-type.js'
import type { ExtractedResourceObjectSchema ,PropertySchemaWithReferenceObject } from '../openapi/deep-extract-resource-object-schemas.js'
// Builds the template context for resource object files (src/Objects/{Name}.php):
// the from_json body lines and the constructor parameter lines.
//
// The blueprint does not track which resource properties are required, so
// every property is optional: from_json falls back to null for missing values
// and the constructor parameters are nullable.

import type {
ResourceObjectProperty,
ResourceObjectSchema,
} from '../resource-model.js'

export interface ObjectLayoutContext {
className: string
fromJsonProps: string[]
constructorParams: string[]
}

const compareProperties = (
[propA, schemaA]: [string, PropertySchemaWithReferenceObject],
[propB, schemaB]: [string, PropertySchemaWithReferenceObject],
requiredPropertyNames: string[],
): number => {
const aRequired = requiredPropertyNames.includes(propA)
const bRequired = requiredPropertyNames.includes(propB)
const aNullable = 'nullable' in schemaA && schemaA.nullable === true
const bNullable = 'nullable' in schemaB && schemaB.nullable === true

if (!aNullable !== !bNullable) return !aNullable ? -1 : 1
if (aRequired !== bRequired) return aRequired ? -1 : 1
return propA.localeCompare(propB)
}

const isObjectReferencingResource = (
schema: PropertySchemaWithReferenceObject,
): boolean =>
'type' in schema &&
schema.type === 'object' &&
'referenceObjectTypeName' in schema
const generateFromJsonProp = (property: ResourceObjectProperty): string => {
const { name } = property

const isArrayReferencingResource = (
schema: PropertySchemaWithReferenceObject,
): boolean =>
'type' in schema &&
schema.type === 'array' &&
'referenceObjectTypeName' in schema

const isPropertyRequired = (
propName: string,
schema: PropertySchemaWithReferenceObject,
requiredPropertyNames: string[],
): boolean =>
'nullable' in schema
? !(schema.nullable ?? false)
: requiredPropertyNames.includes(propName)

const generateFromJsonProp = (
propName: string,
schema: PropertySchemaWithReferenceObject,
requiredPropertyNames: string[],
): string => {
const required = isPropertyRequired(propName, schema, requiredPropertyNames)
const refName =
'referenceObjectTypeName' in schema ? schema.referenceObjectTypeName : ''
switch (property.kind) {
case 'objectReference':
return `${name}: isset($json->${name}) ? ${property.referenceName}::from_json($json->${name}) : null,`

if (isObjectReferencingResource(schema)) {
return `${propName}: ${required ? '' : `isset($json->${propName}) ? `}${refName}::from_json($json->${propName})${required ? ',' : ' : null,'}`
}
case 'listReference':
return `${name}: array_map(fn ($${name[0]}) => ${property.referenceName}::from_json($${name[0]}), $json->${name} ?? []),`

if (isArrayReferencingResource(schema)) {
return `${propName}: array_map(fn ($${propName[0]}) => ${refName}::from_json($${propName[0]}), $json->${propName} ?? []),`
case 'value':
return `${name}: $json->${name} ?? null,`
}

return `${propName}: $json->${propName}${required ? ',' : ' ?? null,'}`
}

const generateConstructorParam = (
propName: string,
schema: PropertySchemaWithReferenceObject,
requiredPropertyNames: string[],
property: ResourceObjectProperty,
): string => {
const phpType = isObjectReferencingResource(schema)
? ((schema as { referenceObjectTypeName?: string })
.referenceObjectTypeName ?? 'mixed')
: getPhpType('type' in schema ? schema.type : 'mixed')
const required = isPropertyRequired(propName, schema, requiredPropertyNames)
const nullSuffix = phpType === 'mixed' ? '' : required ? '' : ' | null'

return `public ${phpType}${nullSuffix} $${propName},`
switch (property.kind) {
case 'objectReference':
return `public ${property.referenceName}|null $${property.name},`

case 'listReference':
return `public array $${property.name},`

case 'value': {
const { phpType } = property
const nullSuffix = phpType === 'mixed' ? '' : '|null'
return `public ${phpType}${nullSuffix} $${property.name},`
}
}
}

export const setObjectLayoutContext = (
schema: ExtractedResourceObjectSchema,
schema: ResourceObjectSchema,
): ObjectLayoutContext => {
const { name, requiredPropertyNames } = schema
const properties = { ...schema.properties }

// TODO: Remove this created_at injection once seam-connect is patched and
// generated output is allowed to change. The nextlove generator added a
// created_at string property to every Errors and Warnings resource class.
if (name.endsWith('Errors') || name.endsWith('Warnings')) {
properties['created_at'] = { type: 'string' }
}

const sorted = Object.entries(properties).sort((a, b) =>
compareProperties(a, b, requiredPropertyNames),
const sorted = [...schema.properties].sort((a, b) =>
a.name.localeCompare(b.name),
)

return {
className: name,
fromJsonProps: sorted.map(([propName, propSchema]) =>
generateFromJsonProp(propName, propSchema, requiredPropertyNames),
),
constructorParams: sorted.map(([propName, propSchema]) =>
generateConstructorParam(propName, propSchema, requiredPropertyNames),
),
className: schema.name,
fromJsonProps: sorted.map(generateFromJsonProp),
constructorParams: sorted.map(generateConstructorParam),
}
}
7 changes: 3 additions & 4 deletions codegen/lib/layouts/seam-client.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
// Builds the template context for src/SeamClient.php.
// Mirrors the nextlove generate-seam-client.ts plus PhpClient#serialize: the
// SeamClient class with its parent client properties, and every resource
// client class with its methods.
// Builds the template context for src/SeamClient.php: the SeamClient class
// with its parent client properties, and every resource client class with its
// methods.

import {
type PhpClient,
Expand Down
22 changes: 8 additions & 14 deletions codegen/lib/map-php-type.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
// TEMPORARY: Verbatim port of @seamapi/nextlove-sdk-generator
// lib/generate-php-sdk/utils/get-php-type.ts. This is a frozen output-parity
// workaround: it exists only so the generated output stays byte-identical to
// the previous generator. Do not review, refactor, or improve it. Note the
// integer case is intentionally absent: it falls through to the default and
// maps to mixed, matching the previous generator (blueprint would collapse
// integer to number and produce float instead).
// TODO: Delete this file and derive types from @seamapi/blueprint parameter
// and resource properties once generated output is allowed to change.
// Maps a blueprint parameter or property to the PHP type used in generated
// declarations.

export const getPhpType = (zodType: string | undefined): string => {
switch (zodType) {
import type { Parameter, Property } from '@seamapi/blueprint'

export const getPhpType = (schema: Parameter | Property): string => {
if (schema.format === 'number' && schema.isInt) return 'int'

switch (schema.jsonType) {
case 'string':
return 'string'

Expand All @@ -19,9 +16,6 @@ export const getPhpType = (zodType: string | undefined): string => {
case 'boolean':
return 'bool'

case 'object':
return 'mixed'

case 'array':
return 'array'

Expand Down
Loading
Loading