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
286 changes: 286 additions & 0 deletions docs/api-reference/_blueprint.json

Large diffs are not rendered by default.

180 changes: 169 additions & 11 deletions mintlify-codegen/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,12 @@ interface CodeGroup {
// Group heading (e.g. "Locks"); null for the ungrouped variants.
name: string | null
entries: CodeEntry[]
// Set on inherited-error groups: the parent resource the errors come from and
// a link to its own errors page. Drives the explanatory callout.
inheritedFrom?: {
noun: string
href?: string
}
}

function isDiscriminatedListProperty(
Expand All @@ -68,6 +74,23 @@ function variantCode(
return prop?.values?.[0]?.name ?? null
}

/** Map a list of variants to sorted code entries, dropping any without a
* discriminator code. */
function variantsToEntries(
variants: DiscriminatedListProperty['variants'],
discriminator: string,
): CodeEntry[] {
return variants
.map((v) => {
const code = variantCode(v, discriminator)
return code == null
? null
: { code, description: (v.description ?? '').trim() }
})
.filter((e): e is CodeEntry => e != null)
.sort((a, b) => a.code.localeCompare(b.code))
}

/**
* Group a resource's `errors` or `warnings` property into ordered code groups:
* the ungrouped variants first (no heading), then each named variant group in
Expand All @@ -78,16 +101,10 @@ function groupCodes(prop: Property | undefined): CodeGroup[] {
if (!isDiscriminatedListProperty(prop)) return []

const entriesFor = (key: string | null): CodeEntry[] =>
prop.variants
.filter((v) => v.variantGroupKey === key)
.map((v) => {
const code = variantCode(v, prop.discriminator)
return code == null
? null
: { code, description: (v.description ?? '').trim() }
})
.filter((e): e is CodeEntry => e != null)
.sort((a, b) => a.code.localeCompare(b.code))
variantsToEntries(
prop.variants.filter((v) => v.variantGroupKey === key),
prop.discriminator,
)

const groups: CodeGroup[] = [{ name: null, entries: entriesFor(null) }]
for (const group of prop.variantGroups) {
Expand All @@ -99,6 +116,97 @@ function groupCodes(prop: Property | undefined): CodeGroup[] {
return groups.filter((g) => g.entries.length > 0)
}

// Resources whose inherited error groups are restricted to an allowlist of
// variant groups (in addition to the always-included ungrouped variants). An
// error's `variant.resourceType` names the resource it belongs to, so a resource
// inherits the errors whose type differs from its own (e.g. an access code
// inherits its lock's device errors). On the access code pages, only
// lock-related inherited errors are relevant; broader device categories like
// thermostats or noise sensors are not.
const INHERITED_ERROR_GROUP_ALLOWLIST: Record<string, string[]> = {
access_code: ['locks'],
unmanaged_access_code: ['locks'],
}

/** Convert a resource type (`connected_account`) into a display noun
* (`Connected Account`) for an inherited-error group heading. */
function resourceTypeNoun(resourceType: string): string {
return resourceType
.split('_')
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
.join(' ')
}

/**
* Group a resource's `errors` property following the inheritance model: first
* the errors that belong to the resource itself (whose `variant.resourceType`
* matches), grouped by variant group exactly like `groupCodes`; then, for each
* parent resource whose errors this resource inherits (variants with a different
* `resourceType`), a single flat group per parent — variant groups are ignored,
* and the group is headed by the parent's noun. Inherited groups are ordered by
* parent resource type. When `inheritedGroupAllowlist` is set, inherited errors
* are limited to the ungrouped variants plus the listed variant groups.
*/
function groupErrorCodes(
prop: Property | undefined,
resourceType: string,
inheritedGroupAllowlist?: string[],
errorsHrefByResourceType?: Record<string, string>,
): CodeGroup[] {
if (!isDiscriminatedListProperty(prop)) return []

// Unmanaged resources carry their managed counterpart's resource type on
// their variants (`unmanaged_access_code` errors are tagged `access_code`), so
// strip the `unmanaged_` prefix to identify the resource's own errors. A
// variant with no resource type is treated as the resource's own.
const ownResourceType = resourceType.replace(/^unmanaged_/, '')
const isOwn = (
variant: DiscriminatedListProperty['variants'][number],
): boolean =>
variant.resourceType == null || variant.resourceType === ownResourceType

// Errors matching the resource's own type, grouped by variant group.
const ownGroups = groupCodes({
...prop,
variants: prop.variants.filter(isOwn),
})

// Errors inherited from parent resources, one flat group per parent resource.
const parentResourceTypes = [
...new Set(
prop.variants
.map((v) => v.resourceType)
.filter((t): t is string => t != null && t !== ownResourceType),
),
].sort()

const isAllowedGroup = (variantGroupKey: string | null): boolean =>
inheritedGroupAllowlist == null ||
variantGroupKey == null ||
inheritedGroupAllowlist.includes(variantGroupKey)

const inheritedGroups: CodeGroup[] = parentResourceTypes
.map((parentResourceType): CodeGroup => {
const variants = prop.variants.filter(
(v) =>
v.resourceType === parentResourceType &&
isAllowedGroup(v.variantGroupKey),
)
const href = errorsHrefByResourceType?.[parentResourceType]
return {
name: resourceTypeNoun(parentResourceType),
entries: variantsToEntries(variants, prop.discriminator),
inheritedFrom: {
noun: resourceTypeNoun(parentResourceType),
...(href == null ? {} : { href }),
},
}
})
.filter((g) => g.entries.length > 0)

return [...ownGroups, ...inheritedGroups]
}

/**
* Order an object's properties for display: the discriminator first, then
* `message` and `created_at`, then everything else alphabetically. Keeps the
Expand Down Expand Up @@ -226,6 +334,24 @@ function renderEntry(entry: CodeEntry, level: string): string {
return [`${level} \`${entry.code}\``, '', description, '', '---'].join('\n')
}

/**
* Render the callout that heads an inherited-error group, explaining that the
* codes belong to a parent resource and are surfaced here when set on it.
*/
function renderInheritedNote(
inheritedFrom: NonNullable<CodeGroup['inheritedFrom']>,
): string {
const { noun, href } = inheritedFrom
const link = href == null ? noun : `[${noun}](${href})`
return [
'<Note>',
` These errors are inherited from the ${link} resource. When they are ` +
`set on the parent ${noun.toLowerCase()}, they are propagated to this ` +
`resource's errors list.`,
'</Note>',
].join('\n')
}

/**
* Render an `## Errors` or `## Warnings` section: the object shape followed by
* every code (as a linkable heading) with its meaning. Returns '' when there are
Expand All @@ -246,6 +372,9 @@ function renderSection(
// ungrouped codes sit directly under the section at `###`.
const codeLevel = group.name != null ? '####' : '###'
if (group.name != null) blocks.push(`### ${group.name}`)
if (group.inheritedFrom != null) {
blocks.push(renderInheritedNote(group.inheritedFrom))
}
for (const entry of group.entries) {
blocks.push(renderEntry(entry, codeLevel))
}
Expand Down Expand Up @@ -349,6 +478,14 @@ interface ErrorPageOptions {
// devices. Used only for device sub-category pages (locks/thermostats/phones),
// which are subsets that omit the device-level codes.
commonDeviceErrorsRoute?: string
// When set, errors are grouped by the inheritance model (own errors first,
// then a flat group per parent resource) keyed on this resource type. Used for
// real per-resource pages; omitted for device sub-category pages, which are
// already scoped to a single variant group.
inheritanceResourceType?: string
// Maps a resource type to its errors page href, used to link an inherited
// group's callout back to the parent resource's own errors page.
errorsHrefByResourceType?: Record<string, string>
}

/**
Expand All @@ -372,7 +509,15 @@ async function writeErrorPage(
return
}

const errorGroups = groupCodes(errorsProp)
const errorGroups =
options.inheritanceResourceType != null
? groupErrorCodes(
errorsProp,
options.inheritanceResourceType,
INHERITED_ERROR_GROUP_ALLOWLIST[options.inheritanceResourceType],
options.errorsHrefByResourceType,
)
: groupCodes(errorsProp)
const warningGroups = groupCodes(warningsProp)
if (errorGroups.length === 0 && warningGroups.length === 0) return

Expand Down Expand Up @@ -511,6 +656,15 @@ export async function updateErrorPages(
): Promise<string[]> {
const routes: string[] = []

// Every documented resource's errors page lives at `/api/<routePath>/errors`,
// so an inherited-error group can link back to the parent it came from.
const errorsHrefByResourceType: Record<string, string> = {}
for (const resource of blueprint.resources) {
if (resource.isUndocumented) continue
errorsHrefByResourceType[resource.resourceType] =
`/api${resource.routePath}/errors`
}

for (const resource of blueprint.resources) {
if (resource.isUndocumented) continue
await writeErrorPage(
Expand All @@ -519,6 +673,10 @@ export async function updateErrorPages(
resource.properties.find((p) => p.name === 'errors'),
resource.properties.find((p) => p.name === 'warnings'),
routes,
{
inheritanceResourceType: resource.resourceType,
errorsHrefByResourceType,
},
)
}

Expand Down
Loading
Loading