mirror of
https://git.openapi.site/https://github.com/desirecore/registry.git
synced 2026-09-05 15:23:39 +08:00
* feat: 建立 Registry 目录元数据验证基础 * feat: 迁移 Registry 应用目录元数据 * test: 解耦目录迁移进度断言 * feat: 迁移 Registry 服务目录元数据 * fix(catalog): 强制 Registry 目录元数据 * fix(catalog): 兼容 Registry v4 外部集成
174 lines
6.9 KiB
JavaScript
174 lines
6.9 KiB
JavaScript
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
|
||
}
|