Skip to content

Commit 254e4ac

Browse files
razor-xclaude
andauthored
feat: Generate one file per route client (#458)
The resource client classes were all serialized into src/SeamClient.php, producing a single ~9000 line file. Generate each client into its own file under src/Routes instead, leaving src/SeamClient.php with just the SeamClient class. Each route file imports only what its methods reference: the SeamClient, the resource classes it deserializes into, and, for ActionAttemptsClient, the errors thrown by poll_until_ready. Child clients share their parent's namespace, so they need no import. The client class bodies are byte identical to the ones previously emitted into src/SeamClient.php, and the classes are PSR-4 autoloaded by the existing Seam\ prefix mapping, so composer.json needs no change. The classes are now under Seam\Routes rather than Seam, matching the Seam\Resources convention. They remain reachable exactly as before through the SeamClient properties, e.g. $seam->acs->users. Claude-Session: https://claude.ai/code/session_01MicfU2S3miuTWZtejb3w8w Co-authored-by: Claude <noreply@anthropic.com>
1 parent 5389000 commit 254e4ac

50 files changed

Lines changed: 9263 additions & 8937 deletions

Some content is hidden

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

.gitattributes

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
11
src/Resources/*.php linguist-generated
2+
src/Routes/*.php linguist-generated
3+
src/SeamClient.php linguist-generated

codegen/layouts/route.hbs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?php
2+
3+
namespace Seam\Routes;
4+
5+
{{#each useStatements}}
6+
use {{this}};
7+
{{/each}}
8+
9+
{{> client-class}}

codegen/layouts/seam-client.hbs

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

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

@@ -12,8 +12,6 @@ use \Exception as Exception;
1212
use Seam\HttpApiError;
1313
use Seam\HttpUnauthorizedError;
1414
use Seam\HttpInvalidInputError;
15-
use Seam\ActionAttemptFailedError;
16-
use Seam\ActionAttemptTimeoutError;
1715

1816
define('LTS_VERSION', '1.0.0');
1917

@@ -100,8 +98,3 @@ class SeamClient
10098
return new Paginator($request, $params);
10199
}
102100
}
103-
104-
{{#each clients}}
105-
{{> client-class}}
106-
107-
{{/each}}

codegen/lib/layouts/route.ts

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
// Builds the template context for route files (src/Routes/{Name}Client.php):
2+
// one resource client class per file, with the use statements for everything
3+
// its methods reference.
4+
5+
import {
6+
type PhpClient,
7+
type PhpClientMethod,
8+
sortPhpClientMethodParameters,
9+
} from '../class-model.js'
10+
11+
const seamClientClass = 'Seam\\SeamClient'
12+
const resourcesNamespace = 'Seam\\Resources'
13+
const actionAttemptErrorClasses = [
14+
'Seam\\ActionAttemptFailedError',
15+
'Seam\\ActionAttemptTimeoutError',
16+
]
17+
18+
export interface MethodLayoutContext {
19+
methodName: string
20+
description: string
21+
responseDescription: string
22+
isDeprecated: boolean
23+
deprecationMessage: string
24+
parameters: Array<{ name: string; type: string; description: string }>
25+
path: string
26+
returnType: string
27+
hasParams: boolean
28+
signatureParams: string
29+
paramNames: string[]
30+
usesActionAttempt: boolean
31+
usesOnResponse: boolean
32+
returnsVoid: boolean
33+
isArrayResponse: boolean
34+
returnResource: string
35+
returnPath: string
36+
}
37+
38+
export interface ClientLayoutContext {
39+
clientName: string
40+
hasChildClients: boolean
41+
childClients: Array<{ clientName: string; namespace: string }>
42+
methods: MethodLayoutContext[]
43+
isActionAttempts: boolean
44+
}
45+
46+
export interface RouteLayoutContext extends ClientLayoutContext {
47+
useStatements: string[]
48+
}
49+
50+
const getMethodLayoutContext = (
51+
method: PhpClientMethod,
52+
clientName: string,
53+
): MethodLayoutContext => {
54+
const { methodName, path, parameters, returnResource, returnPath } = method
55+
56+
const usesActionAttempt =
57+
returnResource === 'ActionAttempt' && clientName !== 'ActionAttempts'
58+
const usesOnResponse =
59+
parameters.some((p) => p.name === 'page_cursor') && methodName === 'list'
60+
const returnsVoid = returnResource === ''
61+
const returnType = method.isArrayResponse
62+
? 'array'
63+
: returnResource !== ''
64+
? returnResource
65+
: 'void'
66+
67+
const sortedParameters = sortPhpClientMethodParameters(parameters)
68+
69+
const signatureParams = sortedParameters
70+
.map(
71+
(p) =>
72+
`${!(p.required ?? false) && p.type !== 'mixed' ? '?' : ''}${p.type} $${p.name}${(p.required ?? false) ? '' : ' = null'}`,
73+
)
74+
.concat(usesActionAttempt ? ['bool $wait_for_action_attempt = true'] : [])
75+
.concat(usesOnResponse ? ['?callable $on_response = null'] : [])
76+
.join(', ')
77+
78+
return {
79+
methodName,
80+
description: method.description,
81+
responseDescription: method.responseDescription,
82+
isDeprecated: method.isDeprecated,
83+
deprecationMessage: method.deprecationMessage,
84+
parameters: sortedParameters.map(({ name, type, description }) => ({
85+
name,
86+
type,
87+
description,
88+
})),
89+
path,
90+
returnType,
91+
hasParams: parameters.length > 0,
92+
signatureParams,
93+
paramNames: sortedParameters.map((p) => p.name),
94+
usesActionAttempt,
95+
usesOnResponse,
96+
returnsVoid,
97+
isArrayResponse: method.isArrayResponse,
98+
returnResource,
99+
returnPath,
100+
}
101+
}
102+
103+
// Child clients live in the same namespace as their parent, so only the
104+
// SeamClient, the resource classes returned by the methods, and the action
105+
// attempt errors thrown by poll_until_ready need importing.
106+
const getUseStatements = (
107+
client: PhpClient,
108+
isActionAttempts: boolean,
109+
): string[] => {
110+
const resourceNames = new Set(
111+
client.methods
112+
.map((m) => m.returnResource)
113+
.filter((resourceName) => resourceName !== ''),
114+
)
115+
116+
if (isActionAttempts) resourceNames.add('ActionAttempt')
117+
118+
return [
119+
seamClientClass,
120+
...[...resourceNames].map((name) => `${resourcesNamespace}\\${name}`),
121+
...(isActionAttempts ? actionAttemptErrorClasses : []),
122+
].sort((a, b) => a.localeCompare(b))
123+
}
124+
125+
export const setRouteLayoutContext = (
126+
client: PhpClient,
127+
): RouteLayoutContext => {
128+
const isActionAttempts = client.clientName === 'ActionAttempts'
129+
130+
return {
131+
useStatements: getUseStatements(client, isActionAttempts),
132+
clientName: client.clientName,
133+
hasChildClients: client.childClientIdentifiers.length > 0,
134+
childClients: client.childClientIdentifiers.map((i) => ({
135+
clientName: i.clientName,
136+
namespace: i.namespace,
137+
})),
138+
methods: client.methods.map((m) =>
139+
getMethodLayoutContext(m, client.clientName),
140+
),
141+
isActionAttempts,
142+
}
143+
}

codegen/lib/layouts/seam-client.ts

Lines changed: 14 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -1,116 +1,27 @@
11
// 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.
2+
// with its parent client properties. The resource client classes themselves
3+
// are generated into src/Routes, one file per client.
44

5-
import {
6-
type PhpClient,
7-
type PhpClientMethod,
8-
sortPhpClientMethodParameters,
9-
} from '../class-model.js'
5+
import type { PhpClient } from '../class-model.js'
106

11-
export interface MethodLayoutContext {
12-
methodName: string
13-
description: string
14-
responseDescription: string
15-
isDeprecated: boolean
16-
deprecationMessage: string
17-
parameters: Array<{ name: string; type: string; description: string }>
18-
path: string
19-
returnType: string
20-
hasParams: boolean
21-
signatureParams: string
22-
paramNames: string[]
23-
usesActionAttempt: boolean
24-
usesOnResponse: boolean
25-
returnsVoid: boolean
26-
isArrayResponse: boolean
27-
returnResource: string
28-
returnPath: string
29-
}
30-
31-
export interface ClientLayoutContext {
32-
clientName: string
33-
hasChildClients: boolean
34-
childClients: Array<{ clientName: string; namespace: string }>
35-
methods: MethodLayoutContext[]
36-
isActionAttempts: boolean
37-
}
7+
const routesNamespace = 'Seam\\Routes'
388

399
export interface SeamClientLayoutContext {
4010
useStatements: string[]
4111
parentClients: Array<{ clientName: string; namespace: string }>
42-
clients: ClientLayoutContext[]
4312
}
4413

45-
const getMethodLayoutContext = (
46-
method: PhpClientMethod,
47-
clientName: string,
48-
): MethodLayoutContext => {
49-
const { methodName, path, parameters, returnResource, returnPath } = method
50-
51-
const usesActionAttempt =
52-
returnResource === 'ActionAttempt' && clientName !== 'ActionAttempts'
53-
const usesOnResponse =
54-
parameters.some((p) => p.name === 'page_cursor') && methodName === 'list'
55-
const returnsVoid = returnResource === ''
56-
const returnType = method.isArrayResponse
57-
? 'array'
58-
: returnResource !== ''
59-
? returnResource
60-
: 'void'
61-
62-
const sortedParameters = sortPhpClientMethodParameters(parameters)
63-
64-
const signatureParams = sortedParameters
65-
.map(
66-
(p) =>
67-
`${!(p.required ?? false) && p.type !== 'mixed' ? '?' : ''}${p.type} $${p.name}${(p.required ?? false) ? '' : ' = null'}`,
68-
)
69-
.concat(usesActionAttempt ? ['bool $wait_for_action_attempt = true'] : [])
70-
.concat(usesOnResponse ? ['?callable $on_response = null'] : [])
71-
.join(', ')
14+
export const setSeamClientLayoutContext = (
15+
clients: PhpClient[],
16+
): SeamClientLayoutContext => {
17+
const parentClients = clients
18+
.filter((c) => c.isParentClient)
19+
.map((c) => ({ clientName: c.clientName, namespace: c.namespace }))
7220

7321
return {
74-
methodName,
75-
description: method.description,
76-
responseDescription: method.responseDescription,
77-
isDeprecated: method.isDeprecated,
78-
deprecationMessage: method.deprecationMessage,
79-
parameters: sortedParameters.map(({ name, type, description }) => ({
80-
name,
81-
type,
82-
description,
83-
})),
84-
path,
85-
returnType,
86-
hasParams: parameters.length > 0,
87-
signatureParams,
88-
paramNames: sortedParameters.map((p) => p.name),
89-
usesActionAttempt,
90-
usesOnResponse,
91-
returnsVoid,
92-
isArrayResponse: method.isArrayResponse,
93-
returnResource,
94-
returnPath,
22+
useStatements: parentClients
23+
.map((c) => `${routesNamespace}\\${c.clientName}Client`)
24+
.sort((a, b) => a.localeCompare(b)),
25+
parentClients,
9526
}
9627
}
97-
98-
export const setSeamClientLayoutContext = (
99-
clients: PhpClient[],
100-
resourceNames: string[],
101-
): SeamClientLayoutContext => ({
102-
useStatements: resourceNames,
103-
parentClients: clients
104-
.filter((c) => c.isParentClient)
105-
.map((c) => ({ clientName: c.clientName, namespace: c.namespace })),
106-
clients: clients.map((c) => ({
107-
clientName: c.clientName,
108-
hasChildClients: c.childClientIdentifiers.length > 0,
109-
childClients: c.childClientIdentifiers.map((i) => ({
110-
clientName: i.clientName,
111-
namespace: i.namespace,
112-
})),
113-
methods: c.methods.map((m) => getMethodLayoutContext(m, c.clientName)),
114-
isActionAttempts: c.clientName === 'ActionAttempts',
115-
})),
116-
})

codegen/lib/routes.ts

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
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 classes written to src/Resources, and the resource client
5-
// classes serialized into src/SeamClient.php.
4+
// resource classes written to src/Resources, the resource client classes
5+
// written to src/Routes, and the SeamClient class in 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'
1212
import { setResourceLayoutContext } from './layouts/resource.js'
13+
import { setRouteLayoutContext } from './layouts/route.js'
1314
import { setSeamClientLayoutContext } from './layouts/seam-client.js'
1415
import { getPhpType } from './map-php-type.js'
1516
import { createResourceModel } from './resource-model.js'
@@ -19,6 +20,7 @@ interface Metadata {
1920
}
2021

2122
const resourcesPath = 'src/Resources'
23+
const routesPath = 'src/Routes'
2224
const seamClientPath = 'src/SeamClient.php'
2325

2426
export const routes = (
@@ -29,9 +31,8 @@ export const routes = (
2931
const { blueprint } = metadata
3032

3133
// 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)
34+
// the local classes for its object properties.
35+
const { resources } = createResourceModel(blueprint)
3536

3637
for (const resource of resources) {
3738
files[`${resourcesPath}/${resource.name}.php`] = {
@@ -41,10 +42,10 @@ export const routes = (
4142
}
4243
}
4344

44-
// Resource client classes, all serialized into SeamClient.php. Each route
45-
// path maps to a client class, e.g. /acs/users to AcsUsersClient, wired to
46-
// a property on its parent client (AcsClient) or, for top-level routes, on
47-
// the SeamClient itself.
45+
// Resource client classes, one file per client. Each route path maps to a
46+
// client class, e.g. /acs/users to AcsUsersClient, wired to a property on
47+
// its parent client (AcsClient) or, for top-level routes, on the SeamClient
48+
// itself.
4849
const classMap = new Map<string, PhpClient>()
4950

5051
const ensureClient = (namespaceSegments: string[]): PhpClient => {
@@ -81,10 +82,20 @@ export const routes = (
8182
}
8283
}
8384

85+
const clients = [...classMap.values()]
86+
87+
for (const client of clients) {
88+
files[`${routesPath}/${client.clientName}Client.php`] = {
89+
contents: Buffer.from('\n'),
90+
layout: 'route.hbs',
91+
...setRouteLayoutContext(client),
92+
}
93+
}
94+
8495
files[seamClientPath] = {
8596
contents: Buffer.from('\n'),
8697
layout: 'seam-client.hbs',
87-
...setSeamClientLayoutContext([...classMap.values()], resourceNames),
98+
...setSeamClientLayoutContext(clients),
8899
}
89100
}
90101

0 commit comments

Comments
 (0)