diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index 4aab7499..59e1af34 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -164,7 +164,7 @@ * [Lock a Lock](api-clients/locks/lock\_door.md) * [Unlock a Lock](api-clients/locks/unlock\_door.md) * [List Locks](api-clients/locks/list.md) -* [Access Control Systems](api-clients/acs/README.md) +* [Access Control Systems](api/acs/README.md) * [Systems](api/acs/systems/README.md) * [List ACS Systems](api/acs/systems/list.md) * [Get an ACS System](api/acs/systems/get.md) diff --git a/docs/api/_report.md b/docs/api/_report.md index 5dd03f4e..0abc218b 100644 --- a/docs/api/_report.md +++ b/docs/api/_report.md @@ -59,7 +59,6 @@ - `/connected_accounts/get` - `/connected_accounts/list` - `/connected_accounts/update` -- `/devices/delete` - `/devices/get` - `/devices/list_device_providers` - `/devices/update` diff --git a/docs/api/acs/README.md b/docs/api/acs/README.md new file mode 100644 index 00000000..e1196e45 --- /dev/null +++ b/docs/api/acs/README.md @@ -0,0 +1,25 @@ +--- +description: Systems for managing and monitoring access to physical spaces +--- + +# Access Control Systems + +## Resources + +### [`acs_system`](./systems/README.md#acs_system) +Represents an [access control system](https://docs.seam.co/latest/capability-guides/access-systems). + +### [`acs_user`](./users/README.md#acs_user) + + +### [`acs_entrance`](./entrances/README.md#acs_entrance) +Represents an [entrance](../../capability-guides/access-systems/retrieving-entrance-details.md) within an [access control system](https://docs.seam.co/latest/capability-guides/access-systems). + +### [`acs_access_group`](./access_groups/README.md#acs_access_group) +Group that defines the entrances to which a set of users has access and, in some cases, the access schedule for these entrances and users. +The `acs_access_group` object represents an [access group](https://docs.seam.co/latest/capability-guides/access-systems/assigning-users-to-access-groups) within an [access control system](https://docs.seam.co/latest/capability-guides/access-systems). + +### [`acs_credential`](./credentials/README.md#acs_credential) +Means by which a user gains access at an entrance. +The `acs_credential` object represents a credential that provides an ACS user access within an access control system. For each acs_credential object, you define the access method. You can also specify additional properties, such as a code. + diff --git a/docs/api/acs/users/README.md b/docs/api/acs/users/README.md index 8f656ec0..69c22336 100644 --- a/docs/api/acs/users/README.md +++ b/docs/api/acs/users/README.md @@ -1,4 +1,4 @@ -# Users +# ACS Users ## `acs_user` diff --git a/src/data/paths.yaml b/src/data/paths.yaml index 3b710080..43446d2d 100644 --- a/src/data/paths.yaml +++ b/src/data/paths.yaml @@ -1,11 +1,15 @@ --- +/acs: + title: Access Control Systems + description: Systems for managing and monitoring access to physical spaces + /acs/systems: title: Systems resources: - acs_system /acs/users: - title: Users + title: ACS Users resources: - acs_user diff --git a/src/layouts/api-namespace.hbs b/src/layouts/api-namespace.hbs new file mode 100644 index 00000000..f99661ba --- /dev/null +++ b/src/layouts/api-namespace.hbs @@ -0,0 +1,13 @@ +--- +description: {{description}} +--- + +# {{title}} + +## Resources + +{{#each resources}} +### [`{{name}}`]({{link}}) +{{description}} + +{{/each}} diff --git a/src/layouts/report.hbs b/src/layouts/report.hbs index a07feba4..fe94181f 100644 --- a/src/layouts/report.hbs +++ b/src/layouts/report.hbs @@ -1,7 +1,15 @@ # Documentation Report -{{#if (or noTitle.routes.length noTitle.endpoints.length )}} +{{#if (or noTitle.routes.length noTitle.endpoints.length noTitle.namespaces.length )}} ## Untitled +{{#if noTitle.namespaces.length }} + +### Namespaces + +{{#each noTitle.namespaces}} +- `{{name}}` +{{/each}} +{{/if}} {{#if noTitle.routes.length }} ### Routes diff --git a/src/lib/handlebars-helpers.ts b/src/lib/handlebars-helpers.ts index fed810cd..fec12fa4 100644 --- a/src/lib/handlebars-helpers.ts +++ b/src/lib/handlebars-helpers.ts @@ -1,3 +1,5 @@ +import { capitalCase } from 'change-case' + export const eq = (v1: unknown, v2: unknown): boolean => { return v1 === v2 } @@ -8,3 +10,11 @@ export const or = (...args: unknown[]): boolean => { return args.some(Boolean) } + +export const add = (v1: number, v2: number): number => { + return v1 + v2 +} + +export const toCapitalCase = (str: string): string => { + return capitalCase(str) +} diff --git a/src/lib/layout/api-namespace.ts b/src/lib/layout/api-namespace.ts new file mode 100644 index 00000000..f25c9913 --- /dev/null +++ b/src/lib/layout/api-namespace.ts @@ -0,0 +1,58 @@ +import type { Blueprint } from '@seamapi/blueprint' + +import type { PathMetadata } from 'lib/path-metadata.js' + +export interface ApiNamespaceLayoutContext { + title: string + description: string + resources: Array<{ + name: string + description: string + link: string + }> +} + +export function setNamespaceLayoutContext( + file: ApiNamespaceLayoutContext, + namespace: string, + resources: Blueprint['resources'], + pathMetadata: PathMetadata, +): void { + const namespaceMetadata = pathMetadata[namespace] + if (namespaceMetadata == null) { + throw new Error(`Namespace metadata for ${namespace} not found`) + } + + file.title = namespaceMetadata.title + file.description = namespaceMetadata.description ?? '' + + const namespaceRoutes = Object.entries(pathMetadata).filter(([p]) => + p.startsWith(namespace), + ) + const namespaceResources = namespaceRoutes.flatMap( + ([_, metadata]) => metadata.resources, + ) + file.resources = namespaceResources.map((resourceName) => { + const resource = resources[resourceName] + + if (resource == null) { + throw new Error(`Resource ${resourceName} not found in blueprint`) + } + + const resourceRoute = namespaceRoutes.find(([_, metadata]) => + metadata.resources.includes(resourceName), + ) + if (resourceRoute == null) { + throw new Error(`Route for resource ${resourceName} not found`) + } + const [routePath] = resourceRoute + const lastPathSegment = routePath.split('/').at(-1) + const docLink = `./${lastPathSegment}/README.md#${resourceName}` + + return { + name: resourceName, + description: resource.description, + link: docLink, + } + }) +} diff --git a/src/lib/layout/index.ts b/src/lib/layout/index.ts index a6401261..964d7e74 100644 --- a/src/lib/layout/index.ts +++ b/src/lib/layout/index.ts @@ -1,2 +1,3 @@ export * from './api-endpoint.js' +export * from './api-namespace.js' export * from './api-route.js' diff --git a/src/lib/path-metadata.ts b/src/lib/path-metadata.ts index dc2cfd8d..b03fa4ef 100644 --- a/src/lib/path-metadata.ts +++ b/src/lib/path-metadata.ts @@ -5,6 +5,7 @@ export const PathMetadataSchema = z.record( z.object({ title: z.string().trim().min(1), resources: z.array(z.string()).default([]), + description: z.string().trim().min(1).optional(), }), ) diff --git a/src/lib/reference.ts b/src/lib/reference.ts index b892c9fd..aa9b415f 100644 --- a/src/lib/reference.ts +++ b/src/lib/reference.ts @@ -1,11 +1,13 @@ -import type { Blueprint } from '@seamapi/blueprint' +import type { Blueprint, Route } from '@seamapi/blueprint' import type Metalsmith from 'metalsmith' import { type ApiEndpointLayoutContext, + type ApiNamespaceLayoutContext, type ApiRouteLayoutContext, setApiRouteLayoutContext, setEndpointLayoutContext, + setNamespaceLayoutContext, } from './layout/index.js' import { PathMetadataSchema } from './path-metadata.js' @@ -14,7 +16,8 @@ const sdks: Array<'javascript'> = [] type Metadata = Partial> type File = ApiEndpointLayoutContext & - ApiRouteLayoutContext & { layout: string } + ApiRouteLayoutContext & + ApiNamespaceLayoutContext & { layout: string } export const reference = ( files: Metalsmith.Files, @@ -35,6 +38,18 @@ export const reference = ( ...metadata, } + const namespacePaths = getNamespacePaths(blueprint.routes) + for (const path of namespacePaths) { + const k = `api${path}/README.md` + files[k] = { + contents: Buffer.from('\n'), + } + const file = files[k] as unknown as File + file.layout = 'api-namespace.hbs' + + setNamespaceLayoutContext(file, path, blueprint.resources, pathMetadata) + } + for (const route of blueprint.routes ?? []) { if ( !route.path.startsWith('/acs') && @@ -79,3 +94,13 @@ export const reference = ( } } } + +function getNamespacePaths(routes: Route[]): string[] { + return Array.from( + new Set( + routes.flatMap((route) => + route.namespace != null ? [route.namespace.path] : [], + ), + ), + ) +} diff --git a/src/lib/report.ts b/src/lib/report.ts index 0db42b53..4dd29a18 100644 --- a/src/lib/report.ts +++ b/src/lib/report.ts @@ -23,7 +23,7 @@ interface Report { deprecated: ReportSection extraResponseKeys: MissingResponseKeyReport[] endpointsWithoutCodeSamples: string[] - noTitle: Pick + noTitle: Pick } interface ReportSection { @@ -81,6 +81,7 @@ function generateReport(metadata: Metadata): Report { extraResponseKeys: [], endpointsWithoutCodeSamples: [], noTitle: { + namespaces: [], routes: [], endpoints: [], }, @@ -204,6 +205,15 @@ function processRoute(route: Route, report: Report, metadata: Metadata): void { 'pathMetadata' in metadata ? PathMetadataSchema.parse(metadata.pathMetadata) : {} + const namespace = route.namespace + if ( + namespace != null && + pathMetadata[namespace.path]?.title == null && + !namespace.isUndocumented + ) { + addUntitledNamespaceToReport(namespace.path, report) + } + if (pathMetadata[route.path]?.title == null && !route.isUndocumented) { report.noTitle.routes.push({ name: route.path }) } @@ -219,6 +229,14 @@ function processRoute(route: Route, report: Report, metadata: Metadata): void { } } +const addUntitledNamespaceToReport = ( + namespace: string, + report: Report, +): void => { + if (report.noTitle.namespaces.some((n) => n.name === namespace)) return + report.noTitle.namespaces.push({ name: namespace }) +} + function processNamespace(namespace: Namespace, report: Report): void { const addNamespace = (section: ReportItem[], reason: string): void => { if (section.some((item) => item.name === namespace.path)) return