File size: 2,608 Bytes
f0634fb
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
import { z } from 'zod';

export function isRecord(value: unknown): value is Record<string, unknown> {
  return typeof value === 'object' && value !== null && !Array.isArray(value);
}

export function truncate(text: string, max = 100): string {
  return text.length > max ? `${text.slice(0, max - 1)}…` : text;
}

export interface JsonSchema {
  readonly $ref?: unknown;
  readonly $defs?: unknown;
  readonly const?: unknown;
  readonly enum?: unknown;
  readonly anyOf?: unknown;
  readonly oneOf?: unknown;
  readonly type?: unknown;
  readonly items?: unknown;
  readonly properties?: unknown;
  readonly required?: unknown;
  readonly additionalProperties?: unknown;
  readonly default?: unknown;
}

export function asJsonSchema(value: unknown): JsonSchema | undefined {
  return isRecord(value) ? (value as JsonSchema) : undefined;
}

export function resolveRef(schema: unknown, root: JsonSchema): unknown {
  const s = asJsonSchema(schema);
  if (typeof s?.$ref === 'string' && s.$ref.startsWith('#/$defs/')) {
    const defs = asJsonSchema(root.$defs);
    const name = s.$ref.slice('#/$defs/'.length);
    if (defs !== undefined && isRecord(defs) && name in defs) {
      return defs[name];
    }
  }
  return schema;
}

export function describeType(
  schema: unknown,
  quoteString: (raw: string) => string = (s) => JSON.stringify(s),
): string {
  const s = asJsonSchema(schema);
  if (s === undefined) return 'any';
  if (s.$ref !== undefined) {
    return typeof s.$ref === 'string' ? (s.$ref.split('/').pop() ?? 'any') : 'any';
  }
  if (s.const !== undefined) {
    return truncate(
      typeof s.const === 'string' ? quoteString(s.const) : JSON.stringify(s.const),
      40,
    );
  }
  if (Array.isArray(s.enum)) {
    return s.enum
      .map((v) => (typeof v === 'string' ? quoteString(v) : JSON.stringify(v)))
      .join(' | ');
  }
  for (const combiner of ['anyOf', 'oneOf'] as const) {
    const subs = s[combiner];
    if (Array.isArray(subs)) return subs.map((sub) => describeType(sub, quoteString)).join(' | ');
  }
  if (s.type === 'array') return `${describeType(s.items, quoteString)}[]`;
  if (s.type === 'object') {
    if (isRecord(s.properties)) return 'object';
    if (isRecord(s.additionalProperties)) {
      return `record<string, ${describeType(s.additionalProperties, quoteString)}>`;
    }
    return 'object';
  }
  if (typeof s.type === 'string') return s.type;
  return 'any';
}

export function toJsonSchema(schema: unknown): JsonSchema | undefined {
  try {
    return z.toJSONSchema(schema as never) as JsonSchema;
  } catch {
    return undefined;
  }
}