File size: 3,058 Bytes
b6ecafa
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
type JsonSchema = {
  type?: string | string[]
  title?: string
  description?: string
  tags?: string[]
  'x-tags'?: string[]
  properties?: Record<string, JsonSchema>
  items?: JsonSchema | JsonSchema[]
  additionalProperties?: JsonSchema | boolean
  enum?: unknown[]
  const?: unknown
  default?: unknown
  anyOf?: JsonSchema[]
  oneOf?: JsonSchema[]
  allOf?: JsonSchema[]
  nullable?: boolean
  minimum?: number
  maximum?: number
  pattern?: string
}

export type { JsonSchema }

/** Resolve the primary type from a schema node */
export function schemaType(schema: JsonSchema | undefined): string | undefined {
  if (!schema) return undefined
  if (Array.isArray(schema.type)) {
    const filtered = schema.type.filter(t => t !== 'null')
    return filtered[0] ?? schema.type[0]
  }
  if (schema.type) return schema.type
  if (schema.properties || schema.additionalProperties) return 'object'
  return undefined
}

/** Normalize union schemas (anyOf/oneOf) into a simpler form */
export function normalizeSchema(schema: JsonSchema): JsonSchema {
  if (!schema.anyOf && !schema.oneOf) return schema

  const union = schema.anyOf ?? schema.oneOf ?? []
  const literals: unknown[] = []
  const remaining: JsonSchema[] = []
  let nullable = false

  for (const entry of union) {
    if (!entry || typeof entry !== 'object') continue
    if (Array.isArray(entry.enum)) {
      for (const v of entry.enum) {
        if (v == null) { nullable = true; continue }
        if (!literals.some(ex => Object.is(ex, v))) literals.push(v)
      }
      continue
    }
    if ('const' in entry) {
      if (entry.const == null) { nullable = true; continue }
      literals.push(entry.const)
      continue
    }
    if (schemaType(entry) === 'null') { nullable = true; continue }
    remaining.push(entry)
  }

  if (literals.length > 0 && remaining.length === 0) {
    return { ...schema, enum: literals, nullable, anyOf: undefined, oneOf: undefined }
  }
  if (remaining.length === 1) {
    return { ...remaining[0], nullable, anyOf: undefined, oneOf: undefined, title: schema.title, description: schema.description }
  }
  return schema
}

/** Infer a field type string from a raw config value (schema-less fallback) */
export function inferFieldType(value: unknown): string {
  if (value == null) return 'string'
  if (typeof value === 'boolean') return 'boolean'
  if (typeof value === 'number') return 'number'
  if (typeof value === 'string') return 'string'
  if (Array.isArray(value)) return 'array'
  if (typeof value === 'object') return 'object'
  return 'string'
}

/** Collect all tags from a schema tree */
export function extractSchemaTags(schema: JsonSchema): string[] {
  const tags = new Set<string>()
  function walk(s: JsonSchema) {
    for (const t of (s['x-tags'] ?? s.tags ?? [])) {
      if (typeof t === 'string') tags.add(t.toLowerCase())
    }
    if (s.properties) {
      for (const child of Object.values(s.properties)) walk(child)
    }
    if (s.items && !Array.isArray(s.items)) walk(s.items)
  }
  walk(schema)
  return [...tags]
}