Skip to content

Commit 6c3352a

Browse files
razor-xclaude
andauthored
Migrate resource classes from Objects to Resources namespace (#454)
Generate the resource classes into src/Resources under the Seam\Resources namespace instead of src/Objects under Seam\Objects. Each blueprint resource now gets a single file. The classes that only exist to type a nested object property of a resource (DeviceBattery, AcsSystemLocation, and so on) are emitted as local classes in the file of the resource that introduced them, rather than as 213 separate files. Since those local classes no longer live in a file matching their name, add a classmap autoload entry for src/Resources alongside the existing PSR-4 mapping. The generated classes themselves are unchanged: all 213 class bodies are byte-identical to the previous output. Claude-Session: https://claude.ai/code/session_014yfWYVy7dgifgtsFPnrkDX Co-authored-by: Claude <noreply@anthropic.com>
1 parent 844a68e commit 6c3352a

237 files changed

Lines changed: 6162 additions & 6666 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.
Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
<?php
22

3-
namespace Seam\Objects;
3+
namespace Seam\Resources;
44

5+
{{#each classes}}
56
class {{className}}
67
{
78
public static function from_json(mixed $json): {{className}}|null
@@ -23,3 +24,5 @@ class {{className}}
2324
) {
2425
}
2526
}
27+
28+
{{/each}}

codegen/layouts/seam-client.hbs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
namespace Seam;
44

55
{{#each useStatements}}
6-
use Seam\Objects\\{{this}};
6+
use Seam\Resources\\{{this}};
77
{{/each}}
88
use Seam\Utils\PackageVersion;
99

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,29 @@
1-
// Builds the template context for resource object files (src/Objects/{Name}.php):
2-
// the from_json body lines and the constructor parameter lines.
1+
// Builds the template context for resource files (src/Resources/{Name}.php):
2+
// the resource class followed by the local classes for its object properties.
3+
// Each class contributes its from_json body lines and constructor parameter
4+
// lines.
35
//
46
// The blueprint does not track which resource properties are required, so
57
// every property is optional: from_json falls back to null for missing values
68
// and the constructor parameters are nullable.
79

810
import type {
9-
ResourceObjectProperty,
10-
ResourceObjectSchema,
11+
ResourceClassProperty,
12+
ResourceClassSchema,
13+
ResourceSchema,
1114
} from '../resource-model.js'
1215

13-
export interface ObjectLayoutContext {
16+
export interface ClassLayoutContext {
1417
className: string
1518
fromJsonProps: string[]
1619
constructorParams: string[]
1720
}
1821

19-
const generateFromJsonProp = (property: ResourceObjectProperty): string => {
22+
export interface ResourceLayoutContext {
23+
classes: ClassLayoutContext[]
24+
}
25+
26+
const generateFromJsonProp = (property: ResourceClassProperty): string => {
2027
const { name } = property
2128

2229
switch (property.kind) {
@@ -31,9 +38,7 @@ const generateFromJsonProp = (property: ResourceObjectProperty): string => {
3138
}
3239
}
3340

34-
const generateConstructorParam = (
35-
property: ResourceObjectProperty,
36-
): string => {
41+
const generateConstructorParam = (property: ResourceClassProperty): string => {
3742
switch (property.kind) {
3843
case 'objectReference':
3944
return `public ${property.referenceName}|null $${property.name},`
@@ -49,9 +54,9 @@ const generateConstructorParam = (
4954
}
5055
}
5156

52-
export const setObjectLayoutContext = (
53-
schema: ResourceObjectSchema,
54-
): ObjectLayoutContext => {
57+
const getClassLayoutContext = (
58+
schema: ResourceClassSchema,
59+
): ClassLayoutContext => {
5560
const sorted = [...schema.properties].sort((a, b) =>
5661
a.name.localeCompare(b.name),
5762
)
@@ -62,3 +67,11 @@ export const setObjectLayoutContext = (
6267
constructorParams: sorted.map(generateConstructorParam),
6368
}
6469
}
70+
71+
export const setResourceLayoutContext = (
72+
resource: ResourceSchema,
73+
): ResourceLayoutContext => ({
74+
classes: [resource.resourceClass, ...resource.localClasses].map(
75+
getClassLayoutContext,
76+
),
77+
})

codegen/lib/layouts/seam-client.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,9 +83,9 @@ const getMethodLayoutContext = (
8383

8484
export const setSeamClientLayoutContext = (
8585
clients: PhpClient[],
86-
baseResourceObjectNames: string[],
86+
resourceNames: string[],
8787
): SeamClientLayoutContext => ({
88-
useStatements: baseResourceObjectNames,
88+
useStatements: resourceNames,
8989
parentClients: clients
9090
.filter((c) => c.isParentClient)
9191
.map((c) => ({ clientName: c.clientName, namespace: c.namespace })),

codegen/lib/resource-model.ts

Lines changed: 66 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1-
// Builds the resource object class model for src/Objects from the blueprint.
1+
// Builds the resource class model for src/Resources from the blueprint.
22
//
3-
// Each blueprint resource becomes a PHP class. Nested object properties and
4-
// lists of objects are split into their own classes named after the base
5-
// resource and the property, e.g. the device battery property becomes
6-
// DeviceBattery. Discriminated unions (events, action attempts, and
3+
// Each blueprint resource becomes a PHP class in its own file. Nested object
4+
// properties and lists of objects are split into their own classes, named
5+
// after the base resource and the property, e.g. the device battery property
6+
// becomes DeviceBattery. Those classes only exist to type a resource
7+
// property, so they are emitted as local classes in the file of the resource
8+
// that introduced them. Discriminated unions (events, action attempts, and
79
// discriminated object lists) are flattened into a single class with the
810
// union of the variant properties.
911

@@ -12,24 +14,28 @@ import { pascalCase } from 'change-case'
1214

1315
import { getPhpType } from './map-php-type.js'
1416

15-
export type ResourceObjectProperty =
17+
export type ResourceClassProperty =
1618
| { name: string; kind: 'value'; phpType: string }
1719
| { name: string; kind: 'objectReference'; referenceName: string }
1820
| { name: string; kind: 'listReference'; referenceName: string }
1921

20-
export interface ResourceObjectSchema {
22+
export interface ResourceClassSchema {
2123
name: string
22-
properties: ResourceObjectProperty[]
24+
properties: ResourceClassProperty[]
2325
}
2426

25-
export interface ResourceObjectModel {
26-
baseResourceNames: string[]
27-
schemas: ResourceObjectSchema[]
27+
export interface ResourceSchema {
28+
name: string
29+
resourceClass: ResourceClassSchema
30+
localClasses: ResourceClassSchema[]
31+
}
32+
33+
export interface ResourceModel {
34+
resourceNames: string[]
35+
resources: ResourceSchema[]
2836
}
2937

30-
export const createResourceObjectModel = (
31-
blueprint: Blueprint,
32-
): ResourceObjectModel => {
38+
export const createResourceModel = (blueprint: Blueprint): ResourceModel => {
3339
const baseResources = new Map<string, Property[]>()
3440

3541
for (const resource of blueprint.resources) {
@@ -57,50 +63,74 @@ export const createResourceObjectModel = (
5763
)
5864
}
5965

60-
const schemas = new Map<string, ResourceObjectSchema>()
66+
const classes = new Map<string, ResourceClassSchema>()
67+
const localClassNames = new Map<string, string[]>()
6168

62-
const addSchema = (
69+
let currentResourceName = ''
70+
71+
const addClass = (
6372
name: string,
6473
properties: Property[],
6574
baseName: string,
6675
): void => {
67-
if (schemas.has(name)) return
68-
const schema: ResourceObjectSchema = { name, properties: [] }
69-
schemas.set(name, schema)
76+
if (classes.has(name)) return
77+
const schema: ResourceClassSchema = { name, properties: [] }
78+
classes.set(name, schema)
79+
if (name !== currentResourceName) {
80+
localClassNames.get(currentResourceName)?.push(name)
81+
}
7082
schema.properties = properties.map((property) =>
71-
createResourceObjectProperty(property, baseName, addSchema),
83+
createResourceClassProperty(property, baseName, addClass),
7284
)
7385
}
7486

7587
const baseResourceTypes = [...baseResources.keys()].sort()
76-
for (const resourceType of baseResourceTypes) {
77-
addSchema(
78-
pascalCase(resourceType),
79-
baseResources.get(resourceType) ?? [],
80-
resourceType,
81-
)
82-
}
88+
const resources = baseResourceTypes.map((resourceType) => {
89+
const name = pascalCase(resourceType)
90+
currentResourceName = name
91+
localClassNames.set(name, [])
92+
addClass(name, baseResources.get(resourceType) ?? [], resourceType)
93+
94+
const resourceClass = classes.get(name)
95+
if (resourceClass == null) {
96+
throw new Error(
97+
`Missing class for resource ${resourceType}: ${name} is already used by a property class of another resource`,
98+
)
99+
}
100+
101+
return {
102+
name,
103+
resourceClass,
104+
localClasses: (localClassNames.get(name) ?? [])
105+
.map((localClassName) => {
106+
const localClass = classes.get(localClassName)
107+
if (localClass == null) {
108+
throw new Error(`Missing local class ${localClassName}`)
109+
}
110+
return localClass
111+
})
112+
.sort((a, b) => a.name.localeCompare(b.name)),
113+
}
114+
})
83115

84116
return {
85-
baseResourceNames: baseResourceTypes.map((resourceType) =>
86-
pascalCase(resourceType),
87-
),
88-
schemas: [...schemas.values()],
117+
resourceNames: resources.map((resource) => resource.name),
118+
resources,
89119
}
90120
}
91121

92-
const createResourceObjectProperty = (
122+
const createResourceClassProperty = (
93123
property: Property,
94124
baseName: string,
95-
addSchema: (name: string, properties: Property[], baseName: string) => void,
96-
): ResourceObjectProperty => {
125+
addClass: (name: string, properties: Property[], baseName: string) => void,
126+
): ResourceClassProperty => {
97127
const referenceName = pascalCase(`${baseName}_${property.name}`)
98128

99129
if (property.format === 'object') {
100130
const { properties } = property
101131

102132
if (properties.length > 0) {
103-
addSchema(referenceName, properties, baseName)
133+
addClass(referenceName, properties, baseName)
104134
return { name: property.name, kind: 'objectReference', referenceName }
105135
}
106136
}
@@ -116,7 +146,7 @@ const createResourceObjectProperty = (
116146
: []
117147

118148
if (itemProperties.length > 0) {
119-
addSchema(referenceName, itemProperties, baseName)
149+
addClass(referenceName, itemProperties, baseName)
120150
return { name: property.name, kind: 'listReference', referenceName }
121151
}
122152
}

codegen/lib/routes.ts

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,24 @@
11
// The Metalsmith plugin that generates the PHP SDK source files.
22
//
33
// The blueprint from @seamapi/blueprint is the only input: it drives the
4-
// resource object classes written to src/Objects, and the resource client
4+
// resource classes written to src/Resources, and the resource client
55
// classes serialized into src/SeamClient.php.
66

77
import type { Blueprint, Endpoint } from '@seamapi/blueprint'
88
import { pascalCase } from 'change-case'
99
import type Metalsmith from 'metalsmith'
1010

1111
import type { PhpClient, PhpClientMethod } from './class-model.js'
12-
import { setObjectLayoutContext } from './layouts/object.js'
12+
import { setResourceLayoutContext } from './layouts/resource.js'
1313
import { setSeamClientLayoutContext } from './layouts/seam-client.js'
1414
import { getPhpType } from './map-php-type.js'
15-
import { createResourceObjectModel } from './resource-model.js'
15+
import { createResourceModel } from './resource-model.js'
1616

1717
interface Metadata {
1818
blueprint: Blueprint
1919
}
2020

21-
const objectsPath = 'src/Objects'
21+
const resourcesPath = 'src/Resources'
2222
const seamClientPath = 'src/SeamClient.php'
2323

2424
export const routes = (
@@ -28,15 +28,16 @@ export const routes = (
2828
const metadata = metalsmith.metadata() as Metadata
2929
const { blueprint } = metadata
3030

31-
// Resource object classes, one file per (deeply extracted) schema. The base
32-
// resource names drive the SeamClient use statements.
33-
const { baseResourceNames, schemas } = createResourceObjectModel(blueprint)
31+
// Resource classes, one file per resource holding the resource class and
32+
// the local classes for its object properties. The resource names drive the
33+
// SeamClient use statements.
34+
const { resourceNames, resources } = createResourceModel(blueprint)
3435

35-
for (const schema of schemas) {
36-
files[`${objectsPath}/${schema.name}.php`] = {
36+
for (const resource of resources) {
37+
files[`${resourcesPath}/${resource.name}.php`] = {
3738
contents: Buffer.from('\n'),
38-
layout: 'object.hbs',
39-
...setObjectLayoutContext(schema),
39+
layout: 'resource.hbs',
40+
...setResourceLayoutContext(resource),
4041
}
4142
}
4243

@@ -83,7 +84,7 @@ export const routes = (
8384
files[seamClientPath] = {
8485
contents: Buffer.from('\n'),
8586
layout: 'seam-client.hbs',
86-
...setSeamClientLayoutContext([...classMap.values()], baseResourceNames),
87+
...setSeamClientLayoutContext([...classMap.values()], resourceNames),
8788
}
8889
}
8990

@@ -93,9 +94,9 @@ const createClientMethod = (endpoint: Endpoint): PhpClientMethod => {
9394
const responseKey = response.responseType === 'void' ? '' : response.responseKey
9495

9596
// Batch responses have no single resource type; they deserialize into the
96-
// Batch resource object. A response whose resource type the blueprint
97-
// cannot resolve ('unknown') has no resource object class to deserialize
98-
// into, so the method is generated as returning void.
97+
// Batch resource. A response whose resource type the blueprint cannot
98+
// resolve ('unknown') has no resource class to deserialize into, so the
99+
// method is generated as returning void.
99100
const resourceType =
100101
response.responseType === 'void'
101102
? ''

codegen/smith.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { helpers, routes } from './lib/index.js'
1111

1212
const rootDir = dirname(fileURLToPath(import.meta.url))
1313

14-
await Promise.all([deleteAsync(['./src/Objects', './src/SeamClient.php'])])
14+
await Promise.all([deleteAsync(['./src/Resources', './src/SeamClient.php'])])
1515

1616
const partials = await getHandlebarsPartials(`${rootDir}/layouts/partials`)
1717

composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88
"psr-4": {
99
"Seam\\": ["src/", "src/Exceptions/"],
1010
"Tests\\": "tests/"
11-
}
11+
},
12+
"classmap": ["src/Resources/"]
1213
},
1314
"authors": [
1415
{

src/Exceptions/ActionAttemptError.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
namespace Seam;
44

5-
use Seam\Objects\ActionAttempt;
5+
use Seam\Resources\ActionAttempt;
66

77
class ActionAttemptError extends \Exception
88
{

src/Exceptions/ActionAttemptFailedError.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
namespace Seam;
44

5-
use Seam\Objects\ActionAttempt;
5+
use Seam\Resources\ActionAttempt;
66

77
class ActionAttemptFailedError extends ActionAttemptError
88
{

0 commit comments

Comments
 (0)