Skip to content

Commit 4dc65fd

Browse files
authored
feat: Fully migrate SDK codegen to use blueprint (#448)
BREAKING CHANGE: The parameter order of two get methods changed so the primary resource ID is now the first parameter. Positional callers must be updated — the old first argument is otherwise silently sent as the wrong parameter: - $seam->events->get(): $event_id is now first (was $device_id). - $seam->acs->users->get(): $acs_user_id is now first (was $acs_system_id). BREAKING CHANGE: ActionAttempt->result is now a typed ActionAttemptResult|null instead of raw decoded JSON. Typed result fields (e.g. acs_credential_on_encoder) are still available as properties; fields that define no type in the spec (result->access_code, result->noise_threshold) are no longer present. BREAKING CHANGE: Resource object constructor parameters are now ordered alphabetically and uniformly nullable. Only affects code constructing resource objects positionally; objects returned by the SDK are unaffected.
1 parent 731abd6 commit 4dc65fd

204 files changed

Lines changed: 1930 additions & 3865 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

codegen/layouts/seam-client.hbs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,6 @@ class SeamClient
6363
];
6464
$options = array_filter($options, fn($option) => $option !== null);
6565

66-
// TODO handle request errors
6766
$response = $this->client->request($method, $path, $options);
6867
$status_code = $response->getStatusCode();
6968
$request_id = $response->getHeaderLine("seam-request-id");

codegen/lib/class-model.ts

Lines changed: 8 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
1-
// Ported from @seamapi/nextlove-sdk-generator
2-
// lib/generate-php-sdk/utils/php-client.ts. Holds the data previously carried
3-
// by the nextlove PhpClient; all string serialization moved to the Handlebars
4-
// layouts and their context builders.
1+
// Data model for the generated resource client classes. All string
2+
// serialization lives in the Handlebars layouts and their context builders.
53

64
export interface PhpClientMethodParameter {
75
name: string
@@ -32,20 +30,12 @@ export interface PhpClient {
3230
methods: PhpClientMethod[]
3331
}
3432

35-
// Verbatim port of the nextlove parameter comparator. The original expression
36-
// `(a.position ?? a.required ? 1000 : 9999)` parses as
37-
// `(a.position ?? a.required) ? 1000 : 9999`, so a parameter with position 0
38-
// is falsy and lands in the 9999 tier together with the optional parameters.
39-
// Combined with a stable sort this yields: required parameters first (in
40-
// schema order), then everything else (in schema order).
41-
// TODO: Fix the operator precedence so position sorts a parameter first as
42-
// originally intended, once generated output is allowed to change. Until then,
43-
// do not "fix" it: the generated output must stay identical.
33+
// Sorts parameters with an explicit position first, then required parameters,
34+
// then everything else, keeping the schema order within each tier.
4435
export const sortPhpClientMethodParameters = (
4536
parameters: PhpClientMethodParameter[],
4637
): PhpClientMethodParameter[] =>
47-
[...parameters].sort(
48-
(a, b) =>
49-
((a.position ?? a.required) ? 1000 : 9999) -
50-
((b.position ?? b.required) ? 1000 : 9999),
51-
)
38+
[...parameters].sort((a, b) => getParameterRank(a) - getParameterRank(b))
39+
40+
const getParameterRank = (parameter: PhpClientMethodParameter): number =>
41+
parameter.position ?? ((parameter.required ?? false) ? 1000 : 9999)

codegen/lib/endpoint-rules.ts

Lines changed: 0 additions & 13 deletions
This file was deleted.

codegen/lib/layouts/object.ts

Lines changed: 40 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -1,114 +1,64 @@
1-
// Builds the template context for resource object files (src/Objects/{Name}.php).
2-
// Mirrors the nextlove generate-resource-object-class.ts serialization: the
3-
// from_json body lines and the constructor parameter lines, each already
4-
// sorted with the nextlove property comparator.
5-
6-
import { getPhpType } from '../map-php-type.js'
7-
import type { ExtractedResourceObjectSchema ,PropertySchemaWithReferenceObject } from '../openapi/deep-extract-resource-object-schemas.js'
1+
// Builds the template context for resource object files (src/Objects/{Name}.php):
2+
// the from_json body lines and the constructor parameter lines.
3+
//
4+
// The blueprint does not track which resource properties are required, so
5+
// every property is optional: from_json falls back to null for missing values
6+
// and the constructor parameters are nullable.
7+
8+
import type {
9+
ResourceObjectProperty,
10+
ResourceObjectSchema,
11+
} from '../resource-model.js'
812

913
export interface ObjectLayoutContext {
1014
className: string
1115
fromJsonProps: string[]
1216
constructorParams: string[]
1317
}
1418

15-
const compareProperties = (
16-
[propA, schemaA]: [string, PropertySchemaWithReferenceObject],
17-
[propB, schemaB]: [string, PropertySchemaWithReferenceObject],
18-
requiredPropertyNames: string[],
19-
): number => {
20-
const aRequired = requiredPropertyNames.includes(propA)
21-
const bRequired = requiredPropertyNames.includes(propB)
22-
const aNullable = 'nullable' in schemaA && schemaA.nullable === true
23-
const bNullable = 'nullable' in schemaB && schemaB.nullable === true
24-
25-
if (!aNullable !== !bNullable) return !aNullable ? -1 : 1
26-
if (aRequired !== bRequired) return aRequired ? -1 : 1
27-
return propA.localeCompare(propB)
28-
}
29-
30-
const isObjectReferencingResource = (
31-
schema: PropertySchemaWithReferenceObject,
32-
): boolean =>
33-
'type' in schema &&
34-
schema.type === 'object' &&
35-
'referenceObjectTypeName' in schema
19+
const generateFromJsonProp = (property: ResourceObjectProperty): string => {
20+
const { name } = property
3621

37-
const isArrayReferencingResource = (
38-
schema: PropertySchemaWithReferenceObject,
39-
): boolean =>
40-
'type' in schema &&
41-
schema.type === 'array' &&
42-
'referenceObjectTypeName' in schema
43-
44-
const isPropertyRequired = (
45-
propName: string,
46-
schema: PropertySchemaWithReferenceObject,
47-
requiredPropertyNames: string[],
48-
): boolean =>
49-
'nullable' in schema
50-
? !(schema.nullable ?? false)
51-
: requiredPropertyNames.includes(propName)
52-
53-
const generateFromJsonProp = (
54-
propName: string,
55-
schema: PropertySchemaWithReferenceObject,
56-
requiredPropertyNames: string[],
57-
): string => {
58-
const required = isPropertyRequired(propName, schema, requiredPropertyNames)
59-
const refName =
60-
'referenceObjectTypeName' in schema ? schema.referenceObjectTypeName : ''
22+
switch (property.kind) {
23+
case 'objectReference':
24+
return `${name}: isset($json->${name}) ? ${property.referenceName}::from_json($json->${name}) : null,`
6125

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

66-
if (isArrayReferencingResource(schema)) {
67-
return `${propName}: array_map(fn ($${propName[0]}) => ${refName}::from_json($${propName[0]}), $json->${propName} ?? []),`
29+
case 'value':
30+
return `${name}: $json->${name} ?? null,`
6831
}
69-
70-
return `${propName}: $json->${propName}${required ? ',' : ' ?? null,'}`
7132
}
7233

7334
const generateConstructorParam = (
74-
propName: string,
75-
schema: PropertySchemaWithReferenceObject,
76-
requiredPropertyNames: string[],
35+
property: ResourceObjectProperty,
7736
): string => {
78-
const phpType = isObjectReferencingResource(schema)
79-
? ((schema as { referenceObjectTypeName?: string })
80-
.referenceObjectTypeName ?? 'mixed')
81-
: getPhpType('type' in schema ? schema.type : 'mixed')
82-
const required = isPropertyRequired(propName, schema, requiredPropertyNames)
83-
const nullSuffix = phpType === 'mixed' ? '' : required ? '' : ' | null'
84-
85-
return `public ${phpType}${nullSuffix} $${propName},`
37+
switch (property.kind) {
38+
case 'objectReference':
39+
return `public ${property.referenceName}|null $${property.name},`
40+
41+
case 'listReference':
42+
return `public array $${property.name},`
43+
44+
case 'value': {
45+
const { phpType } = property
46+
const nullSuffix = phpType === 'mixed' ? '' : '|null'
47+
return `public ${phpType}${nullSuffix} $${property.name},`
48+
}
49+
}
8650
}
8751

8852
export const setObjectLayoutContext = (
89-
schema: ExtractedResourceObjectSchema,
53+
schema: ResourceObjectSchema,
9054
): ObjectLayoutContext => {
91-
const { name, requiredPropertyNames } = schema
92-
const properties = { ...schema.properties }
93-
94-
// TODO: Remove this created_at injection once seam-connect is patched and
95-
// generated output is allowed to change. The nextlove generator added a
96-
// created_at string property to every Errors and Warnings resource class.
97-
if (name.endsWith('Errors') || name.endsWith('Warnings')) {
98-
properties['created_at'] = { type: 'string' }
99-
}
100-
101-
const sorted = Object.entries(properties).sort((a, b) =>
102-
compareProperties(a, b, requiredPropertyNames),
55+
const sorted = [...schema.properties].sort((a, b) =>
56+
a.name.localeCompare(b.name),
10357
)
10458

10559
return {
106-
className: name,
107-
fromJsonProps: sorted.map(([propName, propSchema]) =>
108-
generateFromJsonProp(propName, propSchema, requiredPropertyNames),
109-
),
110-
constructorParams: sorted.map(([propName, propSchema]) =>
111-
generateConstructorParam(propName, propSchema, requiredPropertyNames),
112-
),
60+
className: schema.name,
61+
fromJsonProps: sorted.map(generateFromJsonProp),
62+
constructorParams: sorted.map(generateConstructorParam),
11363
}
11464
}

codegen/lib/layouts/seam-client.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
1-
// Builds the template context for src/SeamClient.php.
2-
// Mirrors the nextlove generate-seam-client.ts plus PhpClient#serialize: the
3-
// SeamClient class with its parent client properties, and every resource
4-
// client class with its methods.
1+
// Builds the template context for src/SeamClient.php: the SeamClient class
2+
// with its parent client properties, and every resource client class with its
3+
// methods.
54

65
import {
76
type PhpClient,

codegen/lib/map-php-type.ts

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

11-
export const getPhpType = (zodType: string | undefined): string => {
12-
switch (zodType) {
4+
import type { Parameter, Property } from '@seamapi/blueprint'
5+
6+
export const getPhpType = (schema: Parameter | Property): string => {
7+
if (schema.format === 'number' && schema.isInt) return 'int'
8+
9+
switch (schema.jsonType) {
1310
case 'string':
1411
return 'string'
1512

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

22-
case 'object':
23-
return 'mixed'
24-
2519
case 'array':
2620
return 'array'
2721

0 commit comments

Comments
 (0)