Files
registry/scripts/catalog/json-schema.mjs
Yige 223be1f04b feat: 建立统一目录元数据与获取契约 (#5)
* feat: 建立 Registry 目录元数据验证基础

* feat: 迁移 Registry 应用目录元数据

* test: 解耦目录迁移进度断言

* feat: 迁移 Registry 服务目录元数据

* fix(catalog): 强制 Registry 目录元数据

* fix(catalog): 兼容 Registry v4 外部集成
2026-08-31 04:03:38 -04:00

174 lines
6.9 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { isDeepStrictEqual } from 'node:util'
function joinPath(base, segment) {
if (typeof segment === 'number') return `${base}[${segment}]`
return base === '$' ? `$.${segment}` : `${base}.${segment}`
}
function resolveRef(rootSchema, ref) {
if (!ref.startsWith('#/')) throw new Error(`只支持本地 JSON Pointer $ref收到 ${ref}`)
return ref
.slice(2)
.split('/')
.map((part) => part.replaceAll('~1', '/').replaceAll('~0', '~'))
.reduce((current, part) => current?.[part], rootSchema)
}
function typeMatches(value, type) {
if (type === 'null') return value === null
if (type === 'array') return Array.isArray(value)
if (type === 'object') return value !== null && typeof value === 'object' && !Array.isArray(value)
if (type === 'integer') return Number.isInteger(value)
if (type === 'number') return typeof value === 'number' && Number.isFinite(value)
return typeof value === type
}
function isUri(value) {
try {
const parsed = new URL(value)
return Boolean(parsed.protocol && parsed.hostname)
} catch {
return false
}
}
function validateNode(value, schema, rootSchema, path, errors) {
if (schema === true) return
if (schema === false) {
errors.push({ path, message: 'Schema 明确拒绝该值' })
return
}
if (!schema || typeof schema !== 'object') return
if (schema.$ref) {
const resolved = resolveRef(rootSchema, schema.$ref)
if (!resolved) {
errors.push({ path, message: `无法解析 $ref ${schema.$ref}` })
return
}
validateNode(value, resolved, rootSchema, path, errors)
return
}
if (Array.isArray(schema.allOf)) {
for (const child of schema.allOf) validateNode(value, child, rootSchema, path, errors)
}
if (Array.isArray(schema.anyOf)) {
const candidates = schema.anyOf.map((child) => {
const childErrors = []
validateNode(value, child, rootSchema, path, childErrors)
return childErrors
})
if (!candidates.some((candidate) => candidate.length === 0)) {
errors.push({ path, message: '值不匹配 anyOf 中的任何分支' })
}
}
if (Array.isArray(schema.oneOf)) {
const candidates = schema.oneOf.map((child) => {
const childErrors = []
validateNode(value, child, rootSchema, path, childErrors)
return childErrors
})
const matches = candidates.filter((candidate) => candidate.length === 0).length
if (matches !== 1) errors.push({ path, message: `值应且仅应匹配 oneOf 的一个分支,实际匹配 ${matches}` })
}
if (schema.not) {
const childErrors = []
validateNode(value, schema.not, rootSchema, path, childErrors)
if (childErrors.length === 0) errors.push({ path, message: '值命中了 not 禁止的结构' })
}
if (schema.if) {
const conditionErrors = []
validateNode(value, schema.if, rootSchema, path, conditionErrors)
const selected = conditionErrors.length === 0 ? schema.then : schema.else
if (selected) validateNode(value, selected, rootSchema, path, errors)
}
if (schema.const !== undefined && !isDeepStrictEqual(value, schema.const)) {
errors.push({ path, message: `必须等于 ${JSON.stringify(schema.const)}` })
}
if (Array.isArray(schema.enum) && !schema.enum.some((candidate) => isDeepStrictEqual(value, candidate))) {
errors.push({ path, message: `必须是 ${schema.enum.map((candidate) => JSON.stringify(candidate)).join(' / ')}` })
}
if (schema.type) {
const allowed = Array.isArray(schema.type) ? schema.type : [schema.type]
if (!allowed.some((type) => typeMatches(value, type))) {
errors.push({ path, message: `类型必须是 ${allowed.join(' / ')}` })
return
}
}
if (typeof value === 'string') {
if (schema.minLength !== undefined && value.length < schema.minLength) {
errors.push({ path, message: `长度不能小于 ${schema.minLength}` })
}
if (schema.maxLength !== undefined && value.length > schema.maxLength) {
errors.push({ path, message: `长度不能大于 ${schema.maxLength}` })
}
if (schema.pattern && !new RegExp(schema.pattern, 'u').test(value)) {
errors.push({ path, message: `不匹配 pattern ${schema.pattern}` })
}
if (schema.format === 'uri' && !isUri(value)) errors.push({ path, message: '必须是绝对 URI' })
if (schema.format === 'date-time' && Number.isNaN(Date.parse(value))) {
errors.push({ path, message: '必须是可解析的 date-time' })
}
}
if (typeof value === 'number') {
if (schema.minimum !== undefined && value < schema.minimum) errors.push({ path, message: `不能小于 ${schema.minimum}` })
if (schema.maximum !== undefined && value > schema.maximum) errors.push({ path, message: `不能大于 ${schema.maximum}` })
}
if (Array.isArray(value)) {
if (schema.minItems !== undefined && value.length < schema.minItems) {
errors.push({ path, message: `元素数量不能少于 ${schema.minItems}` })
}
if (schema.maxItems !== undefined && value.length > schema.maxItems) {
errors.push({ path, message: `元素数量不能多于 ${schema.maxItems}` })
}
if (schema.uniqueItems) {
for (let index = 0; index < value.length; index += 1) {
if (value.slice(0, index).some((candidate) => isDeepStrictEqual(candidate, value[index]))) {
errors.push({ path: joinPath(path, index), message: '数组元素必须唯一' })
}
}
}
if (schema.items) {
value.forEach((item, index) => validateNode(item, schema.items, rootSchema, joinPath(path, index), errors))
}
}
if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
const properties = schema.properties ?? {}
for (const required of schema.required ?? []) {
if (!Object.hasOwn(value, required)) errors.push({ path: joinPath(path, required), message: '缺少必需字段' })
}
if (schema.minProperties !== undefined && Object.keys(value).length < schema.minProperties) {
errors.push({ path, message: `字段数量不能少于 ${schema.minProperties}` })
}
for (const [key, childValue] of Object.entries(value)) {
if (Object.hasOwn(properties, key)) {
validateNode(childValue, properties[key], rootSchema, joinPath(path, key), errors)
} else if (schema.additionalProperties === false) {
errors.push({ path: joinPath(path, key), message: '不允许额外字段' })
} else if (schema.additionalProperties && typeof schema.additionalProperties === 'object') {
validateNode(childValue, schema.additionalProperties, rootSchema, joinPath(path, key), errors)
}
if (schema.propertyNames?.pattern && !new RegExp(schema.propertyNames.pattern, 'u').test(key)) {
errors.push({ path: joinPath(path, key), message: `字段名不匹配 pattern ${schema.propertyNames.pattern}` })
}
}
}
}
export function validateJsonSchema(value, schema) {
const errors = []
validateNode(value, schema, schema, '$', errors)
return errors
}