import { hasOnlyKeys, isRecord } from '@ankhorage/utility/object'; import { isNonEmptyString } from '@ankhorage/utility/string'; import { isStructureDescriptor } from './isStructureDescriptor.js'; import type { StructureDescriptor, StructureDescriptorDefinition, StructureDescriptorDocument, StructureDescriptorId, StructureDescriptorRegistry, } from './types.js'; /*** Validate a complete package structural-descriptor document and all resolvable references. */ export function isStructureDescriptorDocument( value: unknown, ): value is StructureDescriptorDocument { if ( !isRecord(value) || !hasOnlyKeys(value, [ 'protocolVersion', 'packageName', 'packageVersion', 'roots', 'descriptors', ]) || value.protocolVersion !== 1 || !isNonEmptyString(value.packageName) || !isNonEmptyString(value.packageVersion) || !isRootMap(value.roots) || !isDescriptorRegistry(value.descriptors) ) { return false; } const document: StructureDescriptorDocument = { protocolVersion: 1, packageName: value.packageName, packageVersion: value.packageVersion, roots: value.roots, descriptors: value.descriptors, }; return hasResolvableRoots(document) && isDescriptorGraphValid(document); } /*** Validate the root-name to local-descriptor-ID map. */ function isRootMap(value: unknown): value is Readonly> { return ( isRecord(value) && Object.keys(value).length > 0 && Object.entries(value).every( ([name, descriptorId]) => isNonEmptyString(name) && isNonEmptyString(descriptorId), ) ); } /*** Validate descriptor definitions and enforce record-key to definition-ID identity. */ function isDescriptorRegistry(value: unknown): value is StructureDescriptorRegistry { return ( isRecord(value) && Object.keys(value).length > 0 && Object.entries(value).every(([descriptorId, definition]) => isDefinition(definition, descriptorId), ) ); } /*** Validate one descriptor definition against its stable registry key. */ function isDefinition( value: unknown, descriptorId: string, ): value is StructureDescriptorDefinition { return ( isRecord(value) && hasOnlyKeys(value, ['id', 'descriptor']) && value.id === descriptorId && isNonEmptyString(value.id) && isStructureDescriptor(value.descriptor) ); } /*** Require every document root to resolve to one local descriptor definition. */ function hasResolvableRoots(document: StructureDescriptorDocument): boolean { const descriptors = new Map(Object.entries(document.descriptors)); return Object.values(document.roots).every((descriptorId) => descriptors.has(descriptorId)); } /*** Validate local references, recursive graphs, and string-only collection key/member descriptors. */ function isDescriptorGraphValid(document: StructureDescriptorDocument): boolean { const definitions = new Map(Object.entries(document.descriptors)); return Array.from(definitions.values()).every((definition) => validateGraphNode(definition.descriptor, document, new Set()), ); } /*** Validate one graph node while treating revisited local refs as a valid recursive cycle. */ function validateGraphNode( descriptor: StructureDescriptor, document: StructureDescriptorDocument, visitedRefs: Set, ): boolean { switch (descriptor.kind) { case 'object': return Object.values(descriptor.fields).every((field) => validateGraphNode(field.value, document, new Set(visitedRefs)), ); case 'entity-registry': case 'value-map': return ( isStringCompatible(descriptor.key, document, new Set()) && validateGraphNode(descriptor.value, document, new Set(visitedRefs)) ); case 'set': return isStringCompatible(descriptor.member, document, new Set()); case 'ordered-list': return validateGraphNode(descriptor.item, document, new Set(visitedRefs)); case 'union': return descriptor.variants.every((variant) => validateGraphNode(variant, document, new Set(visitedRefs)), ); case 'ref': return validateReference(descriptor, document, visitedRefs); default: return true; } } /*** Resolve and validate one local reference while deferring package-qualified refs to composition. */ function validateReference( descriptor: Extract, document: StructureDescriptorDocument, visitedRefs: Set, ): boolean { if (descriptor.packageName && descriptor.packageName !== document.packageName) return true; if (visitedRefs.has(descriptor.id)) return true; const definition = new Map(Object.entries(document.descriptors)).get(descriptor.id); if (!definition) return false; const nextRefs = new Set(visitedRefs); nextRefs.add(descriptor.id); return validateGraphNode(definition.descriptor, document, nextRefs); } /*** Prove that a collection key/member descriptor is string-valued when locally resolvable. */ function isStringCompatible( descriptor: StructureDescriptor, document: StructureDescriptorDocument, visitedRefs: Set, ): boolean { if (descriptor.kind === 'scalar') return descriptor.type === 'string'; if (descriptor.kind === 'enum') { return descriptor.values.every((value) => typeof value === 'string'); } if (descriptor.kind !== 'ref') return false; if (descriptor.packageName && descriptor.packageName !== document.packageName) return true; if (visitedRefs.has(descriptor.id)) return false; const definition = new Map(Object.entries(document.descriptors)).get(descriptor.id); if (!definition) return false; const nextRefs = new Set(visitedRefs); nextRefs.add(descriptor.id); return isStringCompatible(definition.descriptor, document, nextRefs); }