(schema)
| 734 | * @returns {string[]} One message per problem; empty when valid. |
| 735 | */ |
| 736 | export function checkSchema(schema) { |
| 737 | const errors = [] |
| 738 | const has = (name) => Object.hasOwn(schema.types, name) |
| 739 | const hasUnknown = (node) => |
| 740 | !node |
| 741 | ? false |
| 742 | : node.primitive === 'unknown' |
| 743 | ? true |
| 744 | : node.list |
| 745 | ? hasUnknown(node.list) |
| 746 | : node.map |
| 747 | ? hasUnknown(node.map) |
| 748 | : node.union |
| 749 | ? node.union.some(hasUnknown) |
| 750 | : node.record |
| 751 | ? node.record.some((f) => hasUnknown(f.type)) |
| 752 | : false |
| 753 | const hasEmptyInlineRecord = (node) => |
| 754 | !node |
| 755 | ? false |
| 756 | : Array.isArray(node.record) |
| 757 | ? node.record.length === 0 || node.record.some((f) => hasEmptyInlineRecord(f.type)) |
| 758 | : node.list |
| 759 | ? hasEmptyInlineRecord(node.list) |
| 760 | : node.map |
| 761 | ? hasEmptyInlineRecord(node.map) |
| 762 | : node.union |
| 763 | ? node.union.some(hasEmptyInlineRecord) |
| 764 | : false |
| 765 | const report = (where, node) => { |
| 766 | for (const r of refsInType(node)) if (!has(r)) errors.push(`${where}: unresolved type ${r}`) |
| 767 | if (hasUnknown(node)) errors.push(`${where}: projected to an unknown primitive (unhandled CDDL type)`) |
| 768 | if (hasEmptyInlineRecord(node)) errors.push(`${where}: projected an empty inline record (dropped type reference)`) |
| 769 | } |
| 770 | |
| 771 | for (const c of [...schema.commands, ...schema.events]) { |
| 772 | report(c.method, c.params) |
| 773 | report(c.method, c.result ?? null) |
| 774 | } |
| 775 | // A command/event whose envelope record carries real params must surface them — |
| 776 | // guards the model builder's gap where an inline `params: {...}` (vs a named ref) |
| 777 | // was dropped, leaving it parameterless while its type still required them. |
| 778 | const envelopeParams = commandEnvelopeParams(schema.types) |
| 779 | for (const c of [...schema.commands, ...schema.events]) { |
| 780 | const expected = envelopeParams.get(c.method) |
| 781 | if (expected && c.params?.ref !== expected) |
| 782 | errors.push(`${c.method}: params ${c.params?.ref ?? 'null'} does not match required envelope params ${expected}`) |
| 783 | } |
| 784 | for (const [name, node] of Object.entries(schema.types)) { |
| 785 | if (node.synthetic && !has(node.owner)) errors.push(`${name}: synthetic owner ${node.owner} does not resolve`) |
| 786 | if (node.kind === 'record') { |
| 787 | for (const f of node.fields) report(`${name}.${f.name}`, f.type) |
| 788 | if (node.map) report(`${name}.*`, node.map) |
| 789 | } else if (node.kind === 'union') { |
| 790 | for (const v of node.variants) if (!has(v)) errors.push(`${name}: unresolved variant ${v}`) |
| 791 | errors.push(...checkSelector(name, node.selector, has)) |
| 792 | } else if (node.kind === 'alias') { |
| 793 | report(name, node.type) |
no test coverage detected