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
2 changes: 2 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
src/Resources/*.php linguist-generated
src/Routes/*.php linguist-generated
src/SeamClient.php linguist-generated
9 changes: 9 additions & 0 deletions codegen/layouts/route.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

namespace Seam\Routes;

{{#each useStatements}}
use {{this}};
{{/each}}

{{> client-class}}
9 changes: 1 addition & 8 deletions codegen/layouts/seam-client.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
namespace Seam;

{{#each useStatements}}
use Seam\Resources\\{{this}};
use {{this}};
{{/each}}
use Seam\Utils\PackageVersion;

Expand All @@ -12,8 +12,6 @@ use \Exception as Exception;
use Seam\HttpApiError;
use Seam\HttpUnauthorizedError;
use Seam\HttpInvalidInputError;
use Seam\ActionAttemptFailedError;
use Seam\ActionAttemptTimeoutError;

define('LTS_VERSION', '1.0.0');

Expand Down Expand Up @@ -100,8 +98,3 @@ class SeamClient
return new Paginator($request, $params);
}
}

{{#each clients}}
{{> client-class}}

{{/each}}
143 changes: 143 additions & 0 deletions codegen/lib/layouts/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
// Builds the template context for route files (src/Routes/{Name}Client.php):
// one resource client class per file, with the use statements for everything
// its methods reference.

import {
type PhpClient,
type PhpClientMethod,
sortPhpClientMethodParameters,
} from '../class-model.js'

const seamClientClass = 'Seam\\SeamClient'
const resourcesNamespace = 'Seam\\Resources'
const actionAttemptErrorClasses = [
'Seam\\ActionAttemptFailedError',
'Seam\\ActionAttemptTimeoutError',
]

export interface MethodLayoutContext {
methodName: string
description: string
responseDescription: string
isDeprecated: boolean
deprecationMessage: string
parameters: Array<{ name: string; type: string; description: string }>
path: string
returnType: string
hasParams: boolean
signatureParams: string
paramNames: string[]
usesActionAttempt: boolean
usesOnResponse: boolean
returnsVoid: boolean
isArrayResponse: boolean
returnResource: string
returnPath: string
}

export interface ClientLayoutContext {
clientName: string
hasChildClients: boolean
childClients: Array<{ clientName: string; namespace: string }>
methods: MethodLayoutContext[]
isActionAttempts: boolean
}

export interface RouteLayoutContext extends ClientLayoutContext {
useStatements: string[]
}

const getMethodLayoutContext = (
method: PhpClientMethod,
clientName: string,
): MethodLayoutContext => {
const { methodName, path, parameters, returnResource, returnPath } = method

const usesActionAttempt =
returnResource === 'ActionAttempt' && clientName !== 'ActionAttempts'
const usesOnResponse =
parameters.some((p) => p.name === 'page_cursor') && methodName === 'list'
const returnsVoid = returnResource === ''
const returnType = method.isArrayResponse
? 'array'
: returnResource !== ''
? returnResource
: 'void'

const sortedParameters = sortPhpClientMethodParameters(parameters)

const signatureParams = sortedParameters
.map(
(p) =>
`${!(p.required ?? false) && p.type !== 'mixed' ? '?' : ''}${p.type} $${p.name}${(p.required ?? false) ? '' : ' = null'}`,
)
.concat(usesActionAttempt ? ['bool $wait_for_action_attempt = true'] : [])
.concat(usesOnResponse ? ['?callable $on_response = null'] : [])
.join(', ')

return {
methodName,
description: method.description,
responseDescription: method.responseDescription,
isDeprecated: method.isDeprecated,
deprecationMessage: method.deprecationMessage,
parameters: sortedParameters.map(({ name, type, description }) => ({
name,
type,
description,
})),
path,
returnType,
hasParams: parameters.length > 0,
signatureParams,
paramNames: sortedParameters.map((p) => p.name),
usesActionAttempt,
usesOnResponse,
returnsVoid,
isArrayResponse: method.isArrayResponse,
returnResource,
returnPath,
}
}

// Child clients live in the same namespace as their parent, so only the
// SeamClient, the resource classes returned by the methods, and the action
// attempt errors thrown by poll_until_ready need importing.
const getUseStatements = (
client: PhpClient,
isActionAttempts: boolean,
): string[] => {
const resourceNames = new Set(
client.methods
.map((m) => m.returnResource)
.filter((resourceName) => resourceName !== ''),
)

if (isActionAttempts) resourceNames.add('ActionAttempt')

return [
seamClientClass,
...[...resourceNames].map((name) => `${resourcesNamespace}\\${name}`),
...(isActionAttempts ? actionAttemptErrorClasses : []),
].sort((a, b) => a.localeCompare(b))
}

export const setRouteLayoutContext = (
client: PhpClient,
): RouteLayoutContext => {
const isActionAttempts = client.clientName === 'ActionAttempts'

return {
useStatements: getUseStatements(client, isActionAttempts),
clientName: client.clientName,
hasChildClients: client.childClientIdentifiers.length > 0,
childClients: client.childClientIdentifiers.map((i) => ({
clientName: i.clientName,
namespace: i.namespace,
})),
methods: client.methods.map((m) =>
getMethodLayoutContext(m, client.clientName),
),
isActionAttempts,
}
}
117 changes: 14 additions & 103 deletions codegen/lib/layouts/seam-client.ts
Original file line number Diff line number Diff line change
@@ -1,116 +1,27 @@
// Builds the template context for src/SeamClient.php: the SeamClient class
// with its parent client properties, and every resource client class with its
// methods.
// with its parent client properties. The resource client classes themselves
// are generated into src/Routes, one file per client.

import {
type PhpClient,
type PhpClientMethod,
sortPhpClientMethodParameters,
} from '../class-model.js'
import type { PhpClient } from '../class-model.js'

export interface MethodLayoutContext {
methodName: string
description: string
responseDescription: string
isDeprecated: boolean
deprecationMessage: string
parameters: Array<{ name: string; type: string; description: string }>
path: string
returnType: string
hasParams: boolean
signatureParams: string
paramNames: string[]
usesActionAttempt: boolean
usesOnResponse: boolean
returnsVoid: boolean
isArrayResponse: boolean
returnResource: string
returnPath: string
}

export interface ClientLayoutContext {
clientName: string
hasChildClients: boolean
childClients: Array<{ clientName: string; namespace: string }>
methods: MethodLayoutContext[]
isActionAttempts: boolean
}
const routesNamespace = 'Seam\\Routes'

export interface SeamClientLayoutContext {
useStatements: string[]
parentClients: Array<{ clientName: string; namespace: string }>
clients: ClientLayoutContext[]
}

const getMethodLayoutContext = (
method: PhpClientMethod,
clientName: string,
): MethodLayoutContext => {
const { methodName, path, parameters, returnResource, returnPath } = method

const usesActionAttempt =
returnResource === 'ActionAttempt' && clientName !== 'ActionAttempts'
const usesOnResponse =
parameters.some((p) => p.name === 'page_cursor') && methodName === 'list'
const returnsVoid = returnResource === ''
const returnType = method.isArrayResponse
? 'array'
: returnResource !== ''
? returnResource
: 'void'

const sortedParameters = sortPhpClientMethodParameters(parameters)

const signatureParams = sortedParameters
.map(
(p) =>
`${!(p.required ?? false) && p.type !== 'mixed' ? '?' : ''}${p.type} $${p.name}${(p.required ?? false) ? '' : ' = null'}`,
)
.concat(usesActionAttempt ? ['bool $wait_for_action_attempt = true'] : [])
.concat(usesOnResponse ? ['?callable $on_response = null'] : [])
.join(', ')
export const setSeamClientLayoutContext = (
clients: PhpClient[],
): SeamClientLayoutContext => {
const parentClients = clients
.filter((c) => c.isParentClient)
.map((c) => ({ clientName: c.clientName, namespace: c.namespace }))

return {
methodName,
description: method.description,
responseDescription: method.responseDescription,
isDeprecated: method.isDeprecated,
deprecationMessage: method.deprecationMessage,
parameters: sortedParameters.map(({ name, type, description }) => ({
name,
type,
description,
})),
path,
returnType,
hasParams: parameters.length > 0,
signatureParams,
paramNames: sortedParameters.map((p) => p.name),
usesActionAttempt,
usesOnResponse,
returnsVoid,
isArrayResponse: method.isArrayResponse,
returnResource,
returnPath,
useStatements: parentClients
.map((c) => `${routesNamespace}\\${c.clientName}Client`)
.sort((a, b) => a.localeCompare(b)),
parentClients,
}
}

export const setSeamClientLayoutContext = (
clients: PhpClient[],
resourceNames: string[],
): SeamClientLayoutContext => ({
useStatements: resourceNames,
parentClients: clients
.filter((c) => c.isParentClient)
.map((c) => ({ clientName: c.clientName, namespace: c.namespace })),
clients: clients.map((c) => ({
clientName: c.clientName,
hasChildClients: c.childClientIdentifiers.length > 0,
childClients: c.childClientIdentifiers.map((i) => ({
clientName: i.clientName,
namespace: i.namespace,
})),
methods: c.methods.map((m) => getMethodLayoutContext(m, c.clientName)),
isActionAttempts: c.clientName === 'ActionAttempts',
})),
})
31 changes: 21 additions & 10 deletions codegen/lib/routes.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
// The Metalsmith plugin that generates the PHP SDK source files.
//
// The blueprint from @seamapi/blueprint is the only input: it drives the
// resource classes written to src/Resources, and the resource client
// classes serialized into src/SeamClient.php.
// resource classes written to src/Resources, the resource client classes
// written to src/Routes, and the SeamClient class in src/SeamClient.php.

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

import type { PhpClient, PhpClientMethod } from './class-model.js'
import { setResourceLayoutContext } from './layouts/resource.js'
import { setRouteLayoutContext } from './layouts/route.js'
import { setSeamClientLayoutContext } from './layouts/seam-client.js'
import { getPhpType } from './map-php-type.js'
import { createResourceModel } from './resource-model.js'
Expand All @@ -19,6 +20,7 @@ interface Metadata {
}

const resourcesPath = 'src/Resources'
const routesPath = 'src/Routes'
const seamClientPath = 'src/SeamClient.php'

export const routes = (
Expand All @@ -29,9 +31,8 @@ export const routes = (
const { blueprint } = metadata

// Resource classes, one file per resource holding the resource class and
// the local classes for its object properties. The resource names drive the
// SeamClient use statements.
const { resourceNames, resources } = createResourceModel(blueprint)
// the local classes for its object properties.
const { resources } = createResourceModel(blueprint)

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

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

const ensureClient = (namespaceSegments: string[]): PhpClient => {
Expand Down Expand Up @@ -81,10 +82,20 @@ export const routes = (
}
}

const clients = [...classMap.values()]

for (const client of clients) {
files[`${routesPath}/${client.clientName}Client.php`] = {
contents: Buffer.from('\n'),
layout: 'route.hbs',
...setRouteLayoutContext(client),
}
}

files[seamClientPath] = {
contents: Buffer.from('\n'),
layout: 'seam-client.hbs',
...setSeamClientLayoutContext([...classMap.values()], resourceNames),
...setSeamClientLayoutContext(clients),
}
}

Expand Down
Loading