Skip to content

Commit 11befb5

Browse files
razor-xseambot
andauthored
feat: Add SeamHttpEndpoints (#347)
* feat: Generate SeamHttpEndpoints * Use TS util types * ci: Generate code * Add section to README * Add test for SeamHttpEndpoints --------- Co-authored-by: Seam Bot <[email protected]>
1 parent eb8e0b3 commit 11befb5

File tree

8 files changed

+2076
-18
lines changed

8 files changed

+2076
-18
lines changed

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -512,6 +512,18 @@ const devices = await seam.client.get<DevicesListResponse>('/devices/list')
512512
An Axios compatible client may be provided to create a `SeamHttp` instance.
513513
This API is used internally and is not directly supported.
514514

515+
#### Alternative endpoint path interface
516+
517+
The `SeamHttpEndpoints` class offers an alternative path-based interface to every API endpoint.
518+
Each endpoint is exposed as simple property that returns the corresponding method from `SeamHttp`.
519+
520+
```ts
521+
import { SeamHttpEndpoints } from '@seamapi/http/connect'
522+
523+
const seam = new SeamHttpEndpoints()
524+
const devices = await seam['/devices/list']()
525+
```
526+
515527
#### Inspecting the Request
516528

517529
All client methods return an instance of `SeamHttpRequest`.

codegen/layouts/endpoints.hbs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/*
2+
* Automatically generated by codegen/smith.ts.
3+
* Do not edit this file or add other files to this directory.
4+
*/
5+
6+
{{> route-imports }}
7+
8+
{{#each routeImports}}
9+
import {
10+
{{className}},
11+
{{#each typeNames}}
12+
type {{.}},
13+
{{/each}}
14+
} from './{{fileName}}'
15+
{{/each}}
16+
17+
export class SeamHttpEndpoints {
18+
{{> route-class-methods }}
19+
20+
{{#each endpoints}}
21+
get['{{path}}'](): {{className}}['{{methodName}}']
22+
{
23+
const { client, defaults } = this
24+
return function {{functionName}} (...args: Parameters<{{className}}['{{methodName}}']>): ReturnType<{{className}}['{{methodName}}']>
25+
{
26+
const seam = {{className}}.fromClient(client, defaults)
27+
return seam.{{methodName}}(...args)
28+
}
29+
}
30+
31+
{{/each}}
32+
}

codegen/lib/connect.ts

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,24 @@ import type { Blueprint } from '@seamapi/blueprint'
22
import { kebabCase } from 'change-case'
33
import type Metalsmith from 'metalsmith'
44

5+
import {
6+
type EndpointsLayoutContext,
7+
setEndpointsLayoutContext,
8+
} from './layouts/endpoints.js'
59
import {
610
type RouteIndexLayoutContext,
711
type RouteLayoutContext,
812
setRouteLayoutContext,
13+
toFilePath,
914
} from './layouts/route.js'
1015

1116
interface Metadata {
1217
blueprint: Blueprint
1318
}
1419

15-
type File = RouteLayoutContext & RouteIndexLayoutContext & { layout: string }
20+
type File = RouteLayoutContext &
21+
RouteIndexLayoutContext &
22+
EndpointsLayoutContext & { layout: string }
1623

1724
const rootPath = 'src/lib/seam/connect/routes'
1825

@@ -34,15 +41,22 @@ export const connect = (
3441

3542
const routeIndexes: Record<string, Set<string>> = {}
3643

37-
const rootRouteKey = `${rootPath}/seam-http.ts`
38-
files[rootRouteKey] = { contents: Buffer.from('\n') }
39-
const file = files[rootRouteKey] as unknown as File
44+
const k = `${rootPath}/seam-http.ts`
45+
files[k] = { contents: Buffer.from('\n') }
46+
const file = files[k] as unknown as File
4047
file.layout = 'route.hbs'
4148
setRouteLayoutContext(file, null, nodes)
4249

4350
routeIndexes[''] ??= new Set()
4451
routeIndexes['']?.add('seam-http.js')
4552

53+
const endpointsKey = `${rootPath}/seam-http-endpoints.ts`
54+
files[endpointsKey] = { contents: Buffer.from('\n') }
55+
const endpointFile = files[endpointsKey] as unknown as File
56+
endpointFile.layout = 'endpoints.hbs'
57+
setEndpointsLayoutContext(endpointFile, routes)
58+
routeIndexes['']?.add('seam-http-endpoints.js')
59+
4660
for (const node of nodes) {
4761
const path = toFilePath(node.path)
4862
const name = kebabCase(node.name)
@@ -75,10 +89,3 @@ export const connect = (
7589
file.routes = [...routes]
7690
}
7791
}
78-
79-
const toFilePath = (path: string): string =>
80-
path
81-
.slice(1)
82-
.split('/')
83-
.map((p) => kebabCase(p))
84-
.join('/')

codegen/lib/layouts/endpoints.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import type { Route } from '@seamapi/blueprint'
2+
3+
import {
4+
type EndpointLayoutContext,
5+
getClassName,
6+
getEndpointLayoutContext,
7+
type SubrouteLayoutContext,
8+
toFilePath,
9+
} from './route.js'
10+
11+
export interface EndpointsLayoutContext {
12+
className: string
13+
endpoints: EndpointLayoutContext[]
14+
routeImports: Array<Pick<SubrouteLayoutContext, 'className' | 'fileName'>>
15+
skipClientSessionImport: boolean
16+
}
17+
18+
export const setEndpointsLayoutContext = (
19+
file: Partial<EndpointsLayoutContext>,
20+
routes: Route[],
21+
): void => {
22+
file.className = getClassName('Endpoints')
23+
file.skipClientSessionImport = true
24+
file.endpoints = routes.flatMap((route) =>
25+
route.endpoints
26+
.filter(({ isUndocumented }) => !isUndocumented)
27+
.map((endpoint) => getEndpointLayoutContext(endpoint, route)),
28+
)
29+
file.routeImports = routes.map((route) => {
30+
return {
31+
className: getClassName(route.path),
32+
fileName: `${toFilePath(route.path)}/index.js`,
33+
}
34+
})
35+
}

codegen/lib/layouts/route.ts

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,11 @@ export interface RouteIndexLayoutContext {
1313
routes: string[]
1414
}
1515

16-
interface EndpointLayoutContext {
16+
export interface EndpointLayoutContext {
1717
path: string
1818
methodName: string
19+
functionName: string
20+
className: string
1921
method: Method
2022
hasOptions: boolean
2123
responseKey: string
@@ -30,7 +32,7 @@ interface EndpointLayoutContext {
3032
isOptionalParamsOk: boolean
3133
}
3234

33-
interface SubrouteLayoutContext {
35+
export interface SubrouteLayoutContext {
3436
methodName: string
3537
className: string
3638
fileName: string
@@ -71,7 +73,7 @@ const getSubrouteLayoutContext = (
7173
}
7274
}
7375

74-
const getEndpointLayoutContext = (
76+
export const getEndpointLayoutContext = (
7577
endpoint: Endpoint,
7678
route: Pick<Route, 'path' | 'name'>,
7779
): EndpointLayoutContext => {
@@ -95,11 +97,15 @@ const getEndpointLayoutContext = (
9597
endpoint.response.responseType === 'resource' &&
9698
endpoint.response.resourceType === 'action_attempt'
9799

100+
const methodName = camelCase(endpoint.name)
101+
98102
return {
99103
path: endpoint.path,
100-
methodName: camelCase(endpoint.name),
104+
methodName,
105+
functionName: camelCase(prefix),
101106
method: endpoint.request.preferredMethod,
102107
hasOptions: returnsActionAttempt,
108+
className: getClassName(route.path),
103109
methodParamName,
104110
requestFormat,
105111
requestFormatSuffix,
@@ -129,5 +135,12 @@ const getResponseContext = (
129135
}
130136
}
131137

132-
const getClassName = (name: string | null): string =>
133-
`SeamHttp${pascalCase(name ?? '')}`
138+
export const getClassName = (path: string | null): string =>
139+
`SeamHttp${pascalCase(path ?? '')}`
140+
141+
export const toFilePath = (path: string): string =>
142+
path
143+
.slice(1)
144+
.split('/')
145+
.map((p) => kebabCase(p))
146+
.join('/')

src/lib/seam/connect/routes/index.ts

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)