diff --git a/docs/api/_report.md b/docs/api/_report.md new file mode 100644 index 00000000..b2c2d3aa --- /dev/null +++ b/docs/api/_report.md @@ -0,0 +1,466 @@ +# Documentation Report + +## Undocumented + +These items are intentionally undocumented. + +### Routes + +- `/acs/encoders`: No undocumented message provided +- `/acs/users/unmanaged`: No undocumented message provided + +### Resource properties + +- `acs_user.is_latest_desired_state_synced_with_provider`: Only used internally. +- `acs_user.latest_desired_state_synced_with_provider_at`: Only used internally. + +### Endpoints + +- `/acs/encoders/encode_card`: Encoding a card is currently unimplemented. +- `/acs/encoders/list`: Encoders are in alpha. +- `/acs/encoders/read_card`: Reading a card is currently unimplemented. +- `/acs/users/unmanaged/get`: No unmanaged users are currently implemented. +- `/acs/users/unmanaged/list`: No unmanaged users are currently implemented. + +## No Description + +These items have an empty description. +Items that are intentionally undocumented are not included in this section. + +### Resources + +- `acs_access_group` +- `acs_credential` +- `acs_credential_pool` +- `acs_credential_provisioning_automation` +- `acs_entrance` +- `acs_user` +- `event` +- `thermostat_schedule` + +### Resource properties + +- `acs_access_group.access_group_type` +- `acs_access_group.access_group_type_display_name` +- `acs_access_group.acs_access_group_id` +- `acs_access_group.acs_system_id` +- `acs_access_group.created_at` +- `acs_access_group.display_name` +- `acs_access_group.external_type` +- `acs_access_group.external_type_display_name` +- `acs_access_group.is_managed` +- `acs_access_group.name` +- `acs_access_group.workspace_id` +- `acs_credential.access_method` +- `acs_credential.acs_credential_id` +- `acs_credential.acs_credential_pool_id` +- `acs_credential.acs_system_id` +- `acs_credential.acs_user_id` +- `acs_credential.card_number` +- `acs_credential.code` +- `acs_credential.created_at` +- `acs_credential.display_name` +- `acs_credential.ends_at` +- `acs_credential.errors` +- `acs_credential.external_type` +- `acs_credential.external_type_display_name` +- `acs_credential.is_issued` +- `acs_credential.is_latest_desired_state_synced_with_provider` +- `acs_credential.is_managed` +- `acs_credential.is_multi_phone_sync_credential` +- `acs_credential.issued_at` +- `acs_credential.latest_desired_state_synced_with_provider_at` +- `acs_credential.parent_acs_credential_id` +- `acs_credential.starts_at` +- `acs_credential.visionline_metadata` +- `acs_credential.warnings` +- `acs_credential.workspace_id` +- `acs_credential_pool.acs_credential_pool_id` +- `acs_credential_pool.acs_system_id` +- `acs_credential_pool.created_at` +- `acs_credential_pool.display_name` +- `acs_credential_pool.external_type` +- `acs_credential_pool.external_type_display_name` +- `acs_credential_pool.workspace_id` +- `acs_credential_provisioning_automation.acs_credential_provisioning_automation_id` +- `acs_credential_provisioning_automation.created_at` +- `acs_credential_provisioning_automation.credential_manager_acs_system_id` +- `acs_credential_provisioning_automation.user_identity_id` +- `acs_credential_provisioning_automation.workspace_id` +- `acs_entrance.errors` +- `acs_entrance.latch_metadata` +- `acs_entrance.salto_ks_metadata` +- `acs_entrance.visionline_metadata` +- `acs_system.system_type` +- `acs_system.system_type_display_name` +- `acs_system.visionline_metadata` +- `acs_user.email` +- `acs_user.hid_acs_system_id` +- `acs_user.is_latest_desired_state_synced_with_provider` +- `acs_user.is_managed` +- `acs_user.latest_desired_state_synced_with_provider_at` +- `event.acs_credential_id` +- `event.acs_system_id` +- `event.acs_user_id` +- `event.action_attempt_id` +- `event.client_session_id` +- `event.climate_preset_key` +- `event.cooling_set_point_celsius` +- `event.cooling_set_point_fahrenheit` +- `event.created_at` +- `event.device_id` +- `event.enrollment_automation_id` +- `event.event_description` +- `event.event_id` +- `event.event_type` +- `event.fan_mode_setting` +- `event.heating_set_point_celsius` +- `event.heating_set_point_fahrenheit` +- `event.hvac_mode_setting` +- `event.is_fallback_climate_preset` +- `event.occurred_at` +- `event.thermostat_schedule_id` +- `event.workspace_id` +- `thermostat_schedule.climate_preset_key` +- `thermostat_schedule.created_at` +- `thermostat_schedule.device_id` +- `thermostat_schedule.ends_at` +- `thermostat_schedule.max_override_period_minutes` +- `thermostat_schedule.name` +- `thermostat_schedule.starts_at` +- `thermostat_schedule.thermostat_schedule_id` + +### Endpoints + +- `/acs/access_groups/add_user` +- `/acs/access_groups/get` +- `/acs/access_groups/list` +- `/acs/access_groups/list_accessible_entrances` +- `/acs/access_groups/list_users` +- `/acs/access_groups/remove_user` +- `/acs/access_groups/unmanaged/get` +- `/acs/access_groups/unmanaged/list` +- `/acs/credential_pools/list` +- `/acs/credential_provisioning_automations/launch` +- `/acs/credentials/assign` +- `/acs/credentials/create` +- `/acs/credentials/delete` +- `/acs/credentials/get` +- `/acs/credentials/list` +- `/acs/credentials/list_accessible_entrances` +- `/acs/credentials/unassign` +- `/acs/credentials/update` +- `/acs/credentials/unmanaged/get` +- `/acs/credentials/unmanaged/list` +- `/acs/encoders/encode_card` +- `/acs/encoders/list` +- `/acs/encoders/read_card` +- `/acs/entrances/get` +- `/acs/entrances/grant_access` +- `/acs/entrances/list` +- `/acs/entrances/list_credentials_with_access` +- `/acs/users/unmanaged/get` +- `/acs/users/unmanaged/list` +- `/events/get` +- `/events/list` +- `/thermostats/activate_climate_preset` +- `/thermostats/cool` +- `/thermostats/create_climate_preset` +- `/thermostats/delete_climate_preset` +- `/thermostats/get` +- `/thermostats/heat` +- `/thermostats/heat_cool` +- `/thermostats/list` +- `/thermostats/off` +- `/thermostats/set_fallback_climate_preset` +- `/thermostats/set_fan_mode` +- `/thermostats/update_climate_preset` +- `/thermostats/schedules/create` +- `/thermostats/schedules/delete` +- `/thermostats/schedules/get` +- `/thermostats/schedules/list` +- `/thermostats/schedules/update` + +### Endpoint parameters + +- `/acs/access_groups/add_user` + - `acs_access_group_id` + - `acs_user_id` +- `/acs/access_groups/get` + - `acs_access_group_id` +- `/acs/access_groups/list` + - `acs_system_id` + - `acs_user_id` +- `/acs/access_groups/list_accessible_entrances` + - `acs_access_group_id` +- `/acs/access_groups/list_users` + - `acs_access_group_id` +- `/acs/access_groups/remove_user` + - `acs_access_group_id` + - `acs_user_id` +- `/acs/access_groups/unmanaged/get` + - `acs_access_group_id` +- `/acs/access_groups/unmanaged/list` + - `acs_system_id` + - `acs_user_id` +- `/acs/credential_pools/list` + - `acs_system_id` +- `/acs/credential_provisioning_automations/launch` + - `acs_credential_pool_id` + - `create_credential_manager_user` + - `credential_manager_acs_system_id` + - `credential_manager_acs_user_id` + - `user_identity_id` +- `/acs/credentials/assign` + - `acs_credential_id` + - `acs_user_id` +- `/acs/credentials/create` + - `access_method` + - `acs_user_id` + - `allowed_acs_entrance_ids` + - `code` + - `credential_manager_acs_system_id` + - `ends_at` + - `is_multi_phone_sync_credential` + - `starts_at` + - `visionline_metadata` +- `/acs/credentials/delete` + - `acs_credential_id` +- `/acs/credentials/get` + - `acs_credential_id` +- `/acs/credentials/list_accessible_entrances` + - `acs_credential_id` +- `/acs/credentials/unassign` + - `acs_credential_id` + - `acs_user_id` +- `/acs/credentials/update` + - `acs_credential_id` + - `code` + - `ends_at` +- `/acs/credentials/unmanaged/get` + - `acs_credential_id` +- `/acs/entrances/get` + - `acs_entrance_id` +- `/acs/entrances/grant_access` + - `acs_entrance_id` + - `acs_user_id` +- `/acs/entrances/list` + - `acs_credential_id` + - `acs_system_id` +- `/acs/entrances/list_credentials_with_access` + - `acs_entrance_id` + - `include_if` +- `/acs/users/create` + - `email` +- `/acs/users/list` + - `created_before` + - `limit` +- `/acs/users/update` + - `email` + - `hid_acs_system_id` +- `/acs/users/unmanaged/get` + - `acs_user_id` +- `/acs/users/unmanaged/list` + - `acs_system_id` + - `limit` + - `user_identity_email_address` + - `user_identity_id` + - `user_identity_phone_number` +- `/events/get` + - `device_id` + - `event_id` + - `event_type` +- `/events/list` + - `access_code_id` + - `access_code_ids` + - `between` + - `connect_webview_id` + - `connected_account_id` + - `device_id` + - `device_ids` + - `event_type` + - `event_types` + - `limit` + - `since` +- `/thermostats/activate_climate_preset` + - `climate_preset_key` + - `device_id` +- `/thermostats/cool` + - `cooling_set_point_celsius` + - `cooling_set_point_fahrenheit` + - `device_id` + - `sync` +- `/thermostats/create_climate_preset` + - `climate_preset_key` + - `cooling_set_point_celsius` + - `cooling_set_point_fahrenheit` + - `device_id` + - `fan_mode_setting` + - `heating_set_point_celsius` + - `heating_set_point_fahrenheit` + - `hvac_mode_setting` + - `manual_override_allowed` + - `name` +- `/thermostats/delete_climate_preset` + - `climate_preset_key` + - `device_id` +- `/thermostats/get` + - `device_id` + - `name` +- `/thermostats/heat` + - `device_id` + - `heating_set_point_celsius` + - `heating_set_point_fahrenheit` + - `sync` +- `/thermostats/heat_cool` + - `cooling_set_point_celsius` + - `cooling_set_point_fahrenheit` + - `device_id` + - `heating_set_point_celsius` + - `heating_set_point_fahrenheit` + - `sync` +- `/thermostats/list` + - `connect_webview_id` + - `connected_account_ids` + - `created_before` + - `custom_metadata_has` + - `device_ids` + - `device_types` + - `exclude_if` + - `include_if` + - `limit` + - `manufacturer` + - `user_identifier_key` +- `/thermostats/off` + - `device_id` + - `sync` +- `/thermostats/set_fallback_climate_preset` + - `climate_preset_key` + - `device_id` +- `/thermostats/set_fan_mode` + - `device_id` + - `fan_mode` + - `fan_mode_setting` + - `sync` +- `/thermostats/update_climate_preset` + - `climate_preset_key` + - `cooling_set_point_celsius` + - `cooling_set_point_fahrenheit` + - `device_id` + - `fan_mode_setting` + - `heating_set_point_celsius` + - `heating_set_point_fahrenheit` + - `hvac_mode_setting` + - `manual_override_allowed` + - `name` +- `/thermostats/schedules/create` + - `climate_preset_key` + - `device_id` + - `ends_at` + - `max_override_period_minutes` + - `name` + - `starts_at` +- `/thermostats/schedules/delete` + - `thermostat_schedule_id` +- `/thermostats/schedules/get` + - `thermostat_schedule_id` +- `/thermostats/schedules/list` + - `device_id` + - `user_identifier_key` +- `/thermostats/schedules/update` + - `climate_preset_key` + - `ends_at` + - `max_override_period_minutes` + - `name` + - `starts_at` + - `thermostat_schedule_id` + +## Deprecated + +These items are deprecated. + +### Resource properties + +- `acs_access_group.access_group_type`: use external_type +- `acs_access_group.access_group_type_display_name`: use external_type_display_name +- `acs_system.system_type`: Use `external_type`. +- `acs_system.system_type_display_name`: Use `external_type_display_name`. +- `acs_user.email`: use email_address. + +### Endpoint parameters + +- `/acs/users/create` + - `email`: use email_address. +- `/acs/users/update` + - `email`: use email_address. +- `/thermostats/set_fan_mode` + - `fan_mode`: use fan_mode_setting instead. + +### Extra response keys + +- `/events/get` + - `message` +- `/thermostats/list` + - `thermostats` + +### Endpoints without code samples + +- `/acs/access_groups/add_user` +- `/acs/access_groups/get` +- `/acs/access_groups/list` +- `/acs/access_groups/list_accessible_entrances` +- `/acs/access_groups/list_users` +- `/acs/access_groups/remove_user` +- `/acs/access_groups/unmanaged/get` +- `/acs/access_groups/unmanaged/list` +- `/acs/credential_pools/list` +- `/acs/credential_provisioning_automations/launch` +- `/acs/credentials/assign` +- `/acs/credentials/create` +- `/acs/credentials/delete` +- `/acs/credentials/get` +- `/acs/credentials/list` +- `/acs/credentials/list_accessible_entrances` +- `/acs/credentials/unassign` +- `/acs/credentials/update` +- `/acs/credentials/unmanaged/get` +- `/acs/credentials/unmanaged/list` +- `/acs/encoders/encode_card` +- `/acs/encoders/list` +- `/acs/encoders/read_card` +- `/acs/entrances/get` +- `/acs/entrances/grant_access` +- `/acs/entrances/list` +- `/acs/entrances/list_credentials_with_access` +- `/acs/users/add_to_access_group` +- `/acs/users/create` +- `/acs/users/delete` +- `/acs/users/get` +- `/acs/users/list` +- `/acs/users/list_accessible_entrances` +- `/acs/users/remove_from_access_group` +- `/acs/users/revoke_access_to_all_entrances` +- `/acs/users/suspend` +- `/acs/users/unsuspend` +- `/acs/users/update` +- `/acs/users/unmanaged/get` +- `/acs/users/unmanaged/list` +- `/events/get` +- `/events/list` +- `/thermostats/activate_climate_preset` +- `/thermostats/cool` +- `/thermostats/create_climate_preset` +- `/thermostats/delete_climate_preset` +- `/thermostats/get` +- `/thermostats/heat` +- `/thermostats/heat_cool` +- `/thermostats/list` +- `/thermostats/off` +- `/thermostats/set_fallback_climate_preset` +- `/thermostats/set_fan_mode` +- `/thermostats/update_climate_preset` +- `/thermostats/schedules/create` +- `/thermostats/schedules/delete` +- `/thermostats/schedules/get` +- `/thermostats/schedules/list` +- `/thermostats/schedules/update` diff --git a/src/layouts/report.hbs b/src/layouts/report.hbs new file mode 100644 index 00000000..7a502881 --- /dev/null +++ b/src/layouts/report.hbs @@ -0,0 +1,249 @@ +# Documentation Report +{{#if (or undocumented.routes.length undocumented.resources.length undocumented.resourceProperties.length undocumented.namespaces.length undocumented.endpoints.length undocumented.parameters.length)}} + +## Undocumented + +These items are intentionally undocumented. +{{#if undocumented.routes.length }} + +### Routes + +{{#each undocumented.routes}} +- `{{name}}`: {{reason}} +{{/each}} +{{/if}} +{{#if undocumented.resources.length }} + +### Resources + +{{#each undocumented.resources}} +- `{{name}}`: {{reason}} +{{/each}} +{{/if}} +{{#if undocumented.resourceProperties.length }} + +### Resource properties + +{{#each undocumented.resourceProperties}} +- `{{name}}`: {{reason}} +{{/each}} +{{/if}} +{{#if undocumented.namespaces.length }} + +### Namespaces + +{{#each undocumented.namespaces}} +- `{{name}}`: {{reason}} +{{/each}} +{{/if}} +{{#if undocumented.endpoints.length }} + +### Endpoints + +{{#each undocumented.endpoints}} +- `{{name}}`: {{reason}} +{{/each}} +{{/if}} +{{#if undocumented.parameters.length }} + +### Endpoint parameters + +{{#each undocumented.parameters}} +- `{{path}}` + {{#each params}} + - `{{name}}`: {{reason}} + {{/each}} +{{/each}} +{{/if}} +{{/if}} +{{#if (or noDescription.routes.length noDescription.resources.length noDescription.resourceProperties.length noDescription.namespaces.length noDescription.endpoints.length noDescription.parameters.length)}} + +## No Description + +These items have an empty description. +Items that are intentionally undocumented are not included in this section. +{{#if noDescription.routes.length }} + +### Routes + +{{#each noDescription.routes}} +- `{{name}}` +{{/each}} +{{/if}} +{{#if noDescription.resources.length }} + +### Resources + +{{#each noDescription.resources}} +- `{{name}}` +{{/each}} +{{/if}} +{{#if noDescription.resourceProperties.length }} + +### Resource properties + +{{#each noDescription.resourceProperties}} +- `{{name}}` +{{/each}} +{{/if}} +{{#if noDescription.namespaces.length }} + +### Namespaces + +{{#each noDescription.namespaces}} +- `{{name}}` +{{/each}} +{{/if}} +{{#if noDescription.endpoints.length }} + +### Endpoints + +{{#each noDescription.endpoints}} +- `{{name}}` +{{/each}} +{{/if}} +{{#if noDescription.parameters.length }} + +### Endpoint parameters + +{{#each noDescription.parameters}} +- `{{path}}` + {{#each params}} + - `{{name}}` + {{/each}} +{{/each}} +{{/if}} +{{/if}} +{{#if (or draft.routes.length draft.resources.length draft.resourceProperties.length draft.namespaces.length draft.endpoints.length draft.parameters.length)}} + +## Draft + +These items have been marked as draft. +{{#if draft.routes.length }} + +### Routes + +{{#each draft.routes}} +- `{{name}}`: {{reason}} +{{/each}} +{{/if}} +{{#if draft.resources.length }} + +### Resources + +{{#each draft.resources}} +- `{{name}}`: {{reason}} +{{/each}} +{{/if}} +{{#if draft.resourceProperties.length }} + +### Resource properties + +{{#each draft.resourceProperties}} +- `{{name}}`: {{reason}} +{{/each}} +{{/if}} +{{#if draft.namespaces.length }} + +### Namespaces + +{{#each draft.namespaces}} +- `{{name}}`: {{reason}} +{{/each}} +{{/if}} +{{#if draft.endpoints.length }} + +### Endpoints + +{{#each draft.endpoints}} +- `{{name}}`: {{reason}} +{{/each}} +{{/if}} +{{#if draft.parameters.length }} + +### Endpoint parameters + +{{#each draft.parameters}} +- `{{path}}` + {{#each params}} + - `{{name}}`: {{reason}} + {{/each}} +{{/each}} +{{/if}} +{{/if}} +{{#if (or deprecated.routes.length deprecated.resources.length deprecated.resourceProperties.length deprecated.namespaces.length deprecated.endpoints.length deprecated.parameters.length)}} + +## Deprecated + +These items are deprecated. +{{#if deprecated.routes.length }} + +### Routes + +{{#each deprecated.routes}} +- `{{name}}`: {{reason}} +{{/each}} +{{/if}} +{{#if deprecated.resources.length }} + +### Resources + +{{#each deprecated.resources}} +- `{{name}}`: {{reason}} +{{/each}} +{{/if}} +{{#if deprecated.resourceProperties.length }} + +### Resource properties + +{{#each deprecated.resourceProperties}} +- `{{name}}`: {{reason}} +{{/each}} +{{/if}} +{{#if deprecated.namespaces.length }} + +### Namespaces + +{{#each deprecated.namespaces}} +- `{{name}}`: {{reason}} +{{/each}} +{{/if}} +{{#if deprecated.endpoints.length }} + +### Endpoints + +{{#each deprecated.endpoints}} +- `{{name}}`: {{reason}} +{{/each}} +{{/if}} +{{#if deprecated.parameters.length }} + +### Endpoint parameters + +{{#each deprecated.parameters}} +- `{{path}}` + {{#each params}} + - `{{name}}`: {{reason}} + {{/each}} +{{/each}} +{{/if}} +{{/if}} +{{#if extraResponseKeys.length }} + +### Extra response keys + +{{#each extraResponseKeys}} +- `{{path}}` + {{#each keys}} + - `{{this}}` + {{/each}} +{{/each}} +{{/if}} +{{#if endpointsWithoutCodeSamples.length }} + +### Endpoints without code samples + +{{#each endpointsWithoutCodeSamples}} +- `{{this}}` +{{/each}} +{{/if}} diff --git a/src/lib/handlebars-helpers.ts b/src/lib/handlebars-helpers.ts index be9f946c..fed810cd 100644 --- a/src/lib/handlebars-helpers.ts +++ b/src/lib/handlebars-helpers.ts @@ -1,3 +1,10 @@ export const eq = (v1: unknown, v2: unknown): boolean => { return v1 === v2 } + +export const or = (...args: unknown[]): boolean => { + // remove the last argument, which is the Handlebars options object + args.pop() + + return args.some(Boolean) +} diff --git a/src/lib/index.ts b/src/lib/index.ts index b79f4212..8d0e07a9 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -2,3 +2,4 @@ export * from './blueprint.js' export * as helpers from './handlebars-helpers.js' export * from './postprocess.js' export * from './reference.js' +export * from './report.js' diff --git a/src/lib/report.ts b/src/lib/report.ts new file mode 100644 index 00000000..c0cddc72 --- /dev/null +++ b/src/lib/report.ts @@ -0,0 +1,389 @@ +import type { + Blueprint, + Endpoint, + Namespace, + Parameter, + Property, + Resource, + Route, +} from '@seamapi/blueprint' +import { openapi } from '@seamapi/types/connect' +import type Metalsmith from 'metalsmith' + +const defaultDeprecatedMessage = 'No deprecated message provided' +const defaultDraftMessage = 'No draft message provided' +const defaultUndocumentedMessage = 'No undocumented message provided' + +interface Report { + undocumented: ReportSection + noDescription: ReportSection + draft: ReportSection + deprecated: ReportSection + extraResponseKeys: MissingResponseKeyReport[] + endpointsWithoutCodeSamples: string[] +} + +interface ReportSection { + routes: ReportItem[] + resources: ReportItem[] + resourceProperties: ReportItem[] + namespaces: ReportItem[] + endpoints: ReportItem[] + parameters: ParameterReportItem[] +} + +interface MissingResponseKeyReport { + path: string + keys: string[] +} + +interface ReportItem { + name: string + reason?: string +} + +interface ParameterReportItem { + path: string + params: ReportItem[] +} + +type Metadata = Partial> + +export const report = ( + files: Metalsmith.Files, + metalsmith: Metalsmith, +): void => { + const metadata = { + title: '', + routes: [], + resources: {}, + ...(metalsmith.metadata() as Metadata), + } + + const reportData = generateReport(metadata) + + files['api/_report.md'] = { + contents: Buffer.from('\n'), + layout: 'report.hbs', + ...reportData, + } +} + +function generateReport(metadata: Metadata): Report { + const report: Report = { + undocumented: createEmptyReportSection(), + noDescription: { ...createEmptyReportSection(), resources: [] }, + draft: { ...createEmptyReportSection(), resourceProperties: [] }, + deprecated: createEmptyReportSection(), + extraResponseKeys: [], + endpointsWithoutCodeSamples: [], + } + + const resources = metadata.resources ?? {} + for (const [resourceName, resource] of Object.entries(resources)) { + processResource(resourceName, resource, report) + } + + const routes = metadata.routes ?? [] + for (const route of routes) { + processRoute(route, report) + } + + return report +} + +function createEmptyReportSection(): ReportSection { + return { + resources: [], + resourceProperties: [], + endpoints: [], + parameters: [], + namespaces: [], + routes: [], + } +} + +function processResource( + resourceName: string, + resource: Resource, + report: Report, +): void { + if (resource.description == null || resource.description.trim() === '') { + report.noDescription.resources.push({ name: resourceName }) + } + + if (resource.isDeprecated) { + report.deprecated.resources.push({ + name: resourceName, + reason: resource.deprecationMessage ?? defaultDeprecatedMessage, + }) + } + + if (resource.isUndocumented) { + report.undocumented.resources.push({ + name: resourceName, + reason: resource.undocumentedMessage ?? defaultUndocumentedMessage, + }) + + if (resource.isDraft) { + report.draft.resources.push({ + name: resourceName, + reason: resource.draftMessage ?? defaultDraftMessage, + }) + } + } + + for (const property of resource.properties) { + processProperty(resourceName, property, report) + } +} + +function processProperty( + resourceName: string, + property: Property, + report: Report, +): void { + const propertyName = `${resourceName}.${property.name}` + + if (property.isUndocumented) { + report.undocumented.resourceProperties.push({ + name: propertyName, + reason: property.undocumentedMessage ?? defaultUndocumentedMessage, + }) + } + + if (property.description == null || property.description.trim() === '') { + report.noDescription.resourceProperties.push({ name: propertyName }) + } + + if (property.isDeprecated) { + report.deprecated.resourceProperties.push({ + name: propertyName, + reason: property.deprecationMessage ?? defaultDeprecatedMessage, + }) + } + + if (property.isDraft) { + report.draft.resourceProperties.push({ + name: propertyName, + reason: property.draftMessage ?? defaultDraftMessage, + }) + } +} + +function processRoute(route: Route, report: Report): void { + if (route.isUndocumented) { + report.undocumented.routes.push({ + name: route.path, + reason: defaultUndocumentedMessage, // TODO: undocumentedMessage + }) + } + + if (route.isDeprecated) { + report.deprecated.routes.push({ + name: route.path, + reason: defaultDeprecatedMessage, // TODO: deprecationMessage + }) + } + + if (route.isDraft) { + report.draft.routes.push({ + name: route.path, + reason: defaultDraftMessage, // TODO: draftMessage + }) + } + + if (route.namespace != null) { + processNamespace(route.namespace, report) + } + + // TODO: route description + + for (const endpoint of route.endpoints) { + processEndpoint(endpoint, report) + } +} + +function processNamespace(namespace: Namespace, report: Report): void { + const addNamespace = (section: ReportItem[], reason: string): void => { + if (section.some((item) => item.name === namespace.path)) return + + section.push({ name: namespace.path, reason }) + } + + if (namespace.isDeprecated) { + addNamespace(report.deprecated.namespaces, defaultDeprecatedMessage) + } + + if (namespace.isDraft) { + addNamespace(report.draft.namespaces, defaultDraftMessage) + } + + if (namespace.isUndocumented) { + addNamespace(report.undocumented.namespaces, defaultUndocumentedMessage) + } +} + +function processEndpoint(endpoint: Endpoint, report: Report): void { + if (endpoint.isUndocumented) { + addUniqueEndpoint(report.undocumented.endpoints, { + name: endpoint.path, + reason: endpoint.undocumentedMessage ?? defaultUndocumentedMessage, + }) + } + + if (endpoint.description == null || endpoint.description.trim() === '') { + addUniqueEndpoint(report.noDescription.endpoints, { name: endpoint.path }) + } + + if (endpoint.isDeprecated) { + addUniqueEndpoint(report.deprecated.endpoints, { + name: endpoint.path, + reason: endpoint.deprecationMessage ?? defaultDeprecatedMessage, + }) + } + + if (endpoint.isDraft) { + addUniqueEndpoint(report.draft.endpoints, { + name: endpoint.path, + reason: endpoint.draftMessage ?? defaultDraftMessage, + }) + } + + if (endpoint.codeSamples.length === 0) { + if (!report.endpointsWithoutCodeSamples.includes(endpoint.path)) { + report.endpointsWithoutCodeSamples.push(endpoint.path) + } + } + + processResponseKeys(endpoint, report) + + processParameters(endpoint.path, endpoint.request.parameters, report) +} + +function addUniqueEndpoint( + reportedEndpoints: ReportItem[], + newEndpoint: ReportItem, +): void { + if ( + !reportedEndpoints.some((endpoint) => endpoint.name === newEndpoint.name) + ) { + reportedEndpoints.push(newEndpoint) + } +} + +function processResponseKeys(endpoint: Endpoint, report: Report): void { + if (!('responseKey' in endpoint.response)) return + + const openapiResponseSchemaProps = getOpenapiResponseProperties(endpoint.path) + if (openapiResponseSchemaProps == null) return + + const openapiResponsePropKeys = Object.keys( + openapiResponseSchemaProps, + ).filter((key) => key !== 'ok') + if (openapiResponsePropKeys.length <= 1) return + + const endpointResponseKey = endpoint.response.responseKey + const extraResponseKeys = openapiResponsePropKeys.filter( + (key) => key !== endpointResponseKey, + ) + + report.extraResponseKeys.push({ + path: endpoint.path, + keys: extraResponseKeys, + }) +} + +function getOpenapiResponseProperties( + path: string, +): Record | undefined { + const openapiEndpointDef = openapi.paths[path as keyof typeof openapi.paths] + + if (openapiEndpointDef == null) { + // eslint-disable-next-line no-console + console.warn(`OpenAPI definition not found for endpoint: ${path}`) + return + } + + return openapiEndpointDef.post.responses['200']?.content['application/json'] + ?.schema?.properties +} + +function processParameters( + path: string, + parameters: Parameter[], + report: Report, +): void { + const categorizedParams = parameters.reduce( + (acc, param) => { + if (param.isUndocumented) { + acc.undocumented.push({ + name: param.name, + reason: param.undocumentedMessage ?? defaultUndocumentedMessage, + }) + } + + if (param.description == null || param.description.trim() === '') { + acc.noDescription.push({ name: param.name }) + } + + if (param.isDeprecated) { + acc.deprecated.push({ + name: param.name, + reason: param.deprecationMessage ?? defaultDeprecatedMessage, + }) + } + + if (param.isDraft) { + acc.draft.push({ + name: param.name, + reason: param.draftMessage ?? defaultDraftMessage, + }) + } + + return acc + }, + { + undocumented: [] as ReportItem[], + noDescription: [] as ReportItem[], + deprecated: [] as ReportItem[], + draft: [] as ReportItem[], + }, + ) + + if (categorizedParams.undocumented.length > 0) { + addUniqueParameters(report.undocumented.parameters, { + path, + params: categorizedParams.undocumented, + }) + } + if (categorizedParams.noDescription.length > 0) { + addUniqueParameters(report.noDescription.parameters, { + path, + params: categorizedParams.noDescription, + }) + } + if (categorizedParams.deprecated.length > 0) { + addUniqueParameters(report.deprecated.parameters, { + path, + params: categorizedParams.deprecated, + }) + } + if (categorizedParams.draft.length > 0) { + addUniqueParameters(report.draft.parameters, { + path, + params: categorizedParams.draft, + }) + } +} + +const addUniqueParameters = ( + reportedParams: ParameterReportItem[], + newParam: ParameterReportItem, +): void => { + if (reportedParams.some((param) => param.path === newParam.path)) { + return + } + + reportedParams.push(newParam) +} diff --git a/src/metalsmith.ts b/src/metalsmith.ts index da14831a..8883f81b 100644 --- a/src/metalsmith.ts +++ b/src/metalsmith.ts @@ -6,7 +6,13 @@ import metadata from '@metalsmith/metadata' import { deleteAsync } from 'del' import Metalsmith from 'metalsmith' -import { blueprint, helpers, postprocess, reference } from './lib/index.js' +import { + blueprint, + helpers, + postprocess, + reference, + report, +} from './lib/index.js' const rootDir = dirname(fileURLToPath(import.meta.url)) @@ -23,6 +29,7 @@ Metalsmith(rootDir) ) .use(blueprint) .use(reference) + .use(report) .use( layouts({ default: 'default.hbs',