Skip to content

Commit 9f5b00a

Browse files
committed
feat: align schema handling with GTS spec v0.7
- detect schemas strictly via $schema - normalize gts:// prefixes throughout entity parsing and styling utilities - update package-lock for the new shared helper dependencies Signed-off-by: Artifizer <artifizer@gmail.com>
1 parent 2148e62 commit 9f5b00a

1 file changed

Lines changed: 354 additions & 0 deletions

File tree

Lines changed: 354 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,354 @@
1+
import { GTS_REGEX, GTS_TYPE_REGEX, GTS_OBJ_REGEX, normalizeGtsId } from '@gts/shared'
2+
3+
export interface PropertyInfo {
4+
name: string
5+
type: string
6+
value?: any
7+
required?: boolean
8+
description?: string
9+
children?: PropertyInfo[]
10+
isGtsType?: boolean
11+
isGtsObj?: boolean
12+
}
13+
14+
export function parseJsonToProperties(data: any, name = 'root'): PropertyInfo[] {
15+
if (data === null || data === undefined) {
16+
return [{
17+
name,
18+
type: data === null ? 'null' : 'undefined',
19+
value: data
20+
}]
21+
}
22+
23+
if (typeof data !== 'object') {
24+
return [{
25+
name,
26+
type: typeof data,
27+
value: data
28+
}]
29+
}
30+
31+
if (Array.isArray(data)) {
32+
const children: PropertyInfo[] = data.map((item, index) => {
33+
if (typeof item === 'object' && item !== null) {
34+
return {
35+
name: `[${index}]`,
36+
type: Array.isArray(item) ? 'array' : 'object',
37+
children: parseJsonToProperties(item, `[${index}]`)
38+
}
39+
} else {
40+
return {
41+
name: `[${index}]`,
42+
type: getJsonType(item),
43+
value: item
44+
}
45+
}
46+
})
47+
48+
if (name === 'root') {
49+
return children
50+
}
51+
return [{
52+
name,
53+
type: 'array',
54+
children
55+
}]
56+
}
57+
58+
// Object - directly return the properties without creating a wrapper
59+
const children: PropertyInfo[] = Object.entries(data).map(([key, value]) => {
60+
if (typeof value === 'object' && value !== null) {
61+
if (Array.isArray(value)) {
62+
return {
63+
name: key,
64+
type: 'array',
65+
children: value.map((item, index) => {
66+
if (typeof item === 'object' && item !== null) {
67+
return {
68+
name: `[${index}]`,
69+
type: Array.isArray(item) ? 'array' : 'object',
70+
children: parseJsonToProperties(item, `[${index}]`)
71+
}
72+
} else {
73+
return {
74+
name: `[${index}]`,
75+
type: getJsonType(item),
76+
value: item
77+
}
78+
}
79+
})
80+
}
81+
} else {
82+
// Schema-like object inside arbitrary JSON (e.g., under x-*): delegate entirely to parseSchemaToProperties
83+
const looksSchemaLike = (o: any) => !!(o && (o.$ref || o.type || o.allOf || o.oneOf || o.anyOf || o.properties || o.items || o.enum || o.const))
84+
if (looksSchemaLike(value)) {
85+
const wrapped = parseSchemaToProperties({ properties: { [key]: value } })
86+
const prop = wrapped.find(p => p.name === key)
87+
if (prop) return prop
88+
// Fallback if parsing did not return the property for some reason
89+
return {
90+
name: key,
91+
type: getSchemaType(value),
92+
children: getSchemaChildren(value)
93+
}
94+
}
95+
96+
// For plain nested objects, use recursive parsing without any x-gts-ref special handling
97+
return {
98+
name: key,
99+
type: 'object',
100+
children: parseJsonToProperties(value, key)
101+
}
102+
}
103+
} else {
104+
return {
105+
name: key,
106+
type: getJsonType(value),
107+
value: value
108+
}
109+
}
110+
})
111+
112+
return children
113+
}
114+
115+
export function parseSchemaToProperties(schema: any): PropertyInfo[] {
116+
if (!schema || typeof schema !== 'object') {
117+
return []
118+
}
119+
120+
const properties: PropertyInfo[] = []
121+
122+
// Show extension annotations (x-*) as regular fields
123+
Object.entries(schema)
124+
.filter(([key]) => key.startsWith('x-'))
125+
.forEach(([key, value]) => {
126+
const childProps = typeof value === 'object' && value !== null
127+
? parseJsonToProperties(value, key)
128+
: [{ name: 'value', type: typeof value, value}]
129+
130+
properties.push({
131+
name: key,
132+
type: Array.isArray(value) ? 'array' : typeof value === 'object' ? 'object' : typeof value,
133+
children: childProps
134+
})
135+
})
136+
137+
// Handle schema properties
138+
if (schema.properties) {
139+
const required = schema.required || []
140+
141+
Object.entries(schema.properties).forEach(([key, prop]: [string, any]) => {
142+
let description = prop.description
143+
let isGtsType = false
144+
let isGtsObj = false
145+
146+
// Add $ref information to description (normalize to strip gts:// prefix per GTS spec v0.7)
147+
if (prop.$ref) {
148+
const normalizedRef = normalizeGtsId(prop.$ref)
149+
const refDescription = `Ref: ${normalizedRef}`
150+
description = description ? `${description}\n${refDescription}` : refDescription
151+
if (GTS_TYPE_REGEX.test(normalizedRef)) isGtsType = true
152+
if (GTS_REGEX.test(normalizedRef)) isGtsObj = true
153+
}
154+
155+
// Add pattern information
156+
if (prop.pattern) {
157+
const patDescription = `Pattern: ${prop.pattern}`
158+
description = description ? `${description}\n${patDescription}` : patDescription
159+
}
160+
161+
if (prop['x-gts-ref']) {
162+
const gtsTypeDescription = `GTS Type: ${prop['x-gts-ref']}`
163+
description = description ? `${description}\n${gtsTypeDescription}` : gtsTypeDescription
164+
isGtsType = true
165+
}
166+
167+
properties.push({
168+
name: key,
169+
type: getSchemaType(prop),
170+
required: required.includes(key),
171+
description,
172+
children: getSchemaChildren(prop),
173+
isGtsType,
174+
isGtsObj
175+
})
176+
})
177+
}
178+
179+
// Handle allOf, oneOf, anyOf
180+
if (schema.allOf) {
181+
schema.allOf.forEach((subSchema: any, index: number) => {
182+
183+
const subProperties = parseSchemaToProperties(subSchema)
184+
let description = subSchema.description
185+
let isGtsType = false
186+
let isGtsObj = false
187+
188+
// Normalize $ref to strip gts:// prefix per GTS spec v0.7
189+
const normalizedRef = subSchema.$ref ? normalizeGtsId(subSchema.$ref) : undefined
190+
191+
// Add schema title or $ref information
192+
if (subSchema.title) {
193+
const titleDescription = `Schema: ${subSchema.title}`
194+
description = description ? `${description}\n${titleDescription}` : titleDescription
195+
if (normalizedRef && GTS_TYPE_REGEX.test(normalizedRef)) isGtsType = true
196+
}
197+
198+
if (normalizedRef) {
199+
const refDescription = `Ref: ${normalizedRef}`
200+
description = description ? `${description}\n${refDescription}` : refDescription
201+
if (GTS_TYPE_REGEX.test(normalizedRef)) isGtsType = true
202+
if (GTS_OBJ_REGEX.test(normalizedRef)) isGtsObj = true
203+
}
204+
205+
properties.push({
206+
name: `allOf[${index}]`,
207+
type: 'schema',
208+
description,
209+
children: subProperties,
210+
isGtsType,
211+
isGtsObj
212+
})
213+
})
214+
}
215+
216+
if (schema.oneOf) {
217+
schema.oneOf.forEach((subSchema: any, index: number) => {
218+
const subProperties = parseSchemaToProperties(subSchema)
219+
let description = subSchema.description
220+
221+
// Normalize $ref to strip gts:// prefix per GTS spec v0.7
222+
const normalizedRef = subSchema.$ref ? normalizeGtsId(subSchema.$ref) : undefined
223+
224+
// Add schema title or $ref information
225+
if (subSchema.title) {
226+
const titleDescription = `Schema: ${subSchema.title}`
227+
description = description ? `${description}\n${titleDescription}` : titleDescription
228+
}
229+
if (normalizedRef) {
230+
const refDescription = `Ref: ${normalizedRef}`
231+
description = description ? `${description}\n${refDescription}` : refDescription
232+
}
233+
234+
properties.push({
235+
name: `oneOf[${index}]`,
236+
type: 'schema',
237+
description,
238+
children: subProperties
239+
})
240+
})
241+
}
242+
243+
if (schema.anyOf) {
244+
schema.anyOf.forEach((subSchema: any, index: number) => {
245+
const subProperties = parseSchemaToProperties(subSchema)
246+
let description = subSchema.description
247+
248+
// Normalize $ref to strip gts:// prefix per GTS spec v0.7
249+
const normalizedRef = subSchema.$ref ? normalizeGtsId(subSchema.$ref) : undefined
250+
251+
// Add schema title or $ref information
252+
if (subSchema.title) {
253+
const titleDescription = `Schema: ${subSchema.title}`
254+
description = description ? `${description}\n${titleDescription}` : titleDescription
255+
}
256+
if (normalizedRef) {
257+
const refDescription = `Ref: ${normalizedRef}`
258+
description = description ? `${description}\n${refDescription}` : refDescription
259+
}
260+
261+
properties.push({
262+
name: `anyOf[${index}]`,
263+
type: 'schema',
264+
description,
265+
children: subProperties
266+
})
267+
})
268+
}
269+
270+
// Handle array items
271+
if (schema.type === 'array' && schema.items) {
272+
const itemProperties = parseSchemaToProperties(schema.items)
273+
if (itemProperties.length > 0) {
274+
properties.push({
275+
name: 'items',
276+
type: 'schema',
277+
children: itemProperties
278+
})
279+
}
280+
}
281+
282+
return properties
283+
}
284+
285+
function getJsonType(value: any): string {
286+
if (value === null) return 'null'
287+
if (Array.isArray(value)) return 'array'
288+
return typeof value
289+
}
290+
291+
function getSchemaType(schema: any): string {
292+
if (schema.$ref) {
293+
return '$ref'
294+
}
295+
296+
if (schema.type) {
297+
if (Array.isArray(schema.type)) {
298+
return schema.type.join(' | ')
299+
}
300+
return schema.type
301+
}
302+
303+
if (schema.enum) {
304+
return 'enum'
305+
}
306+
307+
if (schema.const !== undefined) {
308+
return 'const'
309+
}
310+
311+
if (schema.allOf) return 'allOf'
312+
if (schema.oneOf) return 'oneOf'
313+
if (schema.anyOf) return 'anyOf'
314+
315+
return 'unknown'
316+
}
317+
318+
function getSchemaChildren(schema: any): PropertyInfo[] | undefined {
319+
// Handle $ref - don't expand children for references, let the description show the reference
320+
if (schema.$ref) {
321+
return undefined
322+
}
323+
324+
if (schema.type === 'object' && schema.properties) {
325+
return parseSchemaToProperties(schema)
326+
}
327+
328+
if (schema.type === 'array' && schema.items) {
329+
return parseSchemaToProperties(schema.items)
330+
}
331+
332+
if (schema.enum) {
333+
return schema.enum.map((value: any, index: number) => ({
334+
name: `[${index}]`,
335+
type: typeof value,
336+
value
337+
}))
338+
}
339+
340+
if (schema.const !== undefined) {
341+
return [{
342+
name: 'value',
343+
type: typeof schema.const,
344+
value: schema.const
345+
}]
346+
}
347+
348+
// Handle allOf, oneOf, anyOf in children
349+
if (schema.allOf || schema.oneOf || schema.anyOf) {
350+
return parseSchemaToProperties(schema)
351+
}
352+
353+
return undefined
354+
}

0 commit comments

Comments
 (0)