lutaml-jsonschema 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +5 -0
  3. data/CODE_OF_CONDUCT.md +10 -0
  4. data/README.md +39 -0
  5. data/Rakefile +26 -0
  6. data/exe/lutaml-jsonschema +6 -0
  7. data/frontend/index.html +60 -0
  8. data/frontend/package-lock.json +2715 -0
  9. data/frontend/package.json +27 -0
  10. data/frontend/public/lutaml-logo-dark.svg +1 -0
  11. data/frontend/public/lutaml-logo-full-dark.svg +1 -0
  12. data/frontend/public/lutaml-logo-full-light.svg +1 -0
  13. data/frontend/public/lutaml-logo-light.svg +1 -0
  14. data/frontend/src/App.vue +80 -0
  15. data/frontend/src/__tests__/useBuilderField.test.ts +137 -0
  16. data/frontend/src/__tests__/useDefinitionResolver.test.ts +46 -0
  17. data/frontend/src/__tests__/useSchemaTypes.test.ts +219 -0
  18. data/frontend/src/app.ts +10 -0
  19. data/frontend/src/components/AppHeader.vue +152 -0
  20. data/frontend/src/components/AppSidebar.vue +427 -0
  21. data/frontend/src/components/DetailPanel.vue +403 -0
  22. data/frontend/src/components/SchemaBuilder.vue +543 -0
  23. data/frontend/src/components/SchemaStructure.vue +168 -0
  24. data/frontend/src/components/SearchModal.vue +275 -0
  25. data/frontend/src/composables/useBuilderField.ts +92 -0
  26. data/frontend/src/composables/useDefinitionResolver.ts +17 -0
  27. data/frontend/src/composables/useSchemaTypes.ts +152 -0
  28. data/frontend/src/composables/useSearch.ts +104 -0
  29. data/frontend/src/router.ts +14 -0
  30. data/frontend/src/stores/schemaStore.ts +118 -0
  31. data/frontend/src/stores/uiStore.ts +78 -0
  32. data/frontend/src/style.css +194 -0
  33. data/frontend/src/types.ts +70 -0
  34. data/frontend/src/views/HomeView.vue +396 -0
  35. data/frontend/tsconfig.json +20 -0
  36. data/frontend/vite.config.ts +28 -0
  37. data/lib/lutaml/jsonschema/base.rb +11 -0
  38. data/lib/lutaml/jsonschema/cli.rb +102 -0
  39. data/lib/lutaml/jsonschema/combiner.rb +54 -0
  40. data/lib/lutaml/jsonschema/configuration.rb +47 -0
  41. data/lib/lutaml/jsonschema/link.rb +25 -0
  42. data/lib/lutaml/jsonschema/property_entry.rb +15 -0
  43. data/lib/lutaml/jsonschema/reference_resolver.rb +74 -0
  44. data/lib/lutaml/jsonschema/schema.rb +205 -0
  45. data/lib/lutaml/jsonschema/schema_set.rb +217 -0
  46. data/lib/lutaml/jsonschema/spa/generator.rb +22 -0
  47. data/lib/lutaml/jsonschema/spa/metadata.rb +23 -0
  48. data/lib/lutaml/jsonschema/spa/output_strategy.rb +17 -0
  49. data/lib/lutaml/jsonschema/spa/spa_builder.rb +178 -0
  50. data/lib/lutaml/jsonschema/spa/spa_definition.rb +27 -0
  51. data/lib/lutaml/jsonschema/spa/spa_document.rb +23 -0
  52. data/lib/lutaml/jsonschema/spa/spa_property.rb +47 -0
  53. data/lib/lutaml/jsonschema/spa/spa_schema.rb +29 -0
  54. data/lib/lutaml/jsonschema/spa/spa_search_entry.rb +21 -0
  55. data/lib/lutaml/jsonschema/spa/vue_inlined_strategy.rb +53 -0
  56. data/lib/lutaml/jsonschema/version.rb +7 -0
  57. data/lib/lutaml/jsonschema.rb +29 -0
  58. data/sig/lutaml/jsonschema.rbs +6 -0
  59. metadata +163 -0
@@ -0,0 +1,403 @@
1
+ <template>
2
+ <div class="detail-panel-overlay" @click.self="uiStore.closeDetailPanel">
3
+ <aside class="detail-panel">
4
+ <div class="panel-header">
5
+ <div class="panel-title">
6
+ <h2 v-if="item">{{ itemTitle }}</h2>
7
+ </div>
8
+ <button class="btn btn-ghost" @click="uiStore.closeDetailPanel">
9
+ <svg width="18" height="18" viewBox="0 0 18 18" fill="none">
10
+ <path d="M5 5l8 8M13 5l-8 8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
11
+ </svg>
12
+ </button>
13
+ </div>
14
+
15
+ <div v-if="item" class="panel-tabs">
16
+ <button
17
+ v-for="tab in currentTabs"
18
+ :key="tab.id"
19
+ class="panel-tab"
20
+ :class="{ active: uiStore.activePanelTab === tab.id }"
21
+ @click="uiStore.setPanelTab(tab.id)"
22
+ >
23
+ {{ tab.label }}
24
+ </button>
25
+ </div>
26
+
27
+ <div class="panel-content">
28
+ <template v-if="item">
29
+ <!-- Overview tab -->
30
+ <template v-if="uiStore.activePanelTab === 'overview'">
31
+ <div class="detail-section">
32
+ <div class="detail-meta">
33
+ <div v-if="item.kind === 'schema' || item.kind === 'definition'" class="meta-row">
34
+ <span class="meta-label">Type</span>
35
+ <span class="badge badge-type">{{ itemType }}</span>
36
+ </div>
37
+ <div v-if="item.kind === 'property' && (item as any).property.format" class="meta-row">
38
+ <span class="meta-label">Format</span>
39
+ <span class="badge badge-format">{{ (item as any).property.format }}</span>
40
+ </div>
41
+ <div v-if="itemDescription" class="meta-row">
42
+ <span class="meta-label">Description</span>
43
+ <span class="text-secondary">{{ itemDescription }}</span>
44
+ </div>
45
+ <div v-if="item.kind === 'property'" class="meta-row">
46
+ <span class="meta-label">Required</span>
47
+ <span :class="['badge', (item as any).property.required ? 'badge-required' : 'badge-optional']">
48
+ {{ (item as any).property.required ? 'Yes' : 'No' }}
49
+ </span>
50
+ </div>
51
+ <div v-if="item.kind === 'property' && (item as any).property.deprecated" class="meta-row">
52
+ <span class="meta-label">Status</span>
53
+ <span class="badge badge-deprecated">Deprecated</span>
54
+ </div>
55
+ <div v-if="item.kind === 'property' && (item as any).property.ref" class="meta-row">
56
+ <span class="meta-label">Reference</span>
57
+ <span class="font-mono text-secondary">{{ (item as any).property.ref }}</span>
58
+ </div>
59
+ <div v-if="item.kind === 'property' && (item as any).property.default" class="meta-row">
60
+ <span class="meta-label">Default</span>
61
+ <span class="font-mono">{{ (item as any).property.default }}</span>
62
+ </div>
63
+ <div v-if="item.kind === 'schema' && schema.required.length" class="meta-row">
64
+ <span class="meta-label">Required</span>
65
+ <div class="meta-tags">
66
+ <span v-for="r in schema.required" :key="r" class="badge badge-required-sm">{{ r }}</span>
67
+ </div>
68
+ </div>
69
+ </div>
70
+ </div>
71
+
72
+ <!-- Constraints for property -->
73
+ <div v-if="item.kind === 'property' && hasConstraints" class="detail-section">
74
+ <h3 class="detail-heading">Constraints</h3>
75
+ <table class="table constraint-table">
76
+ <tbody>
77
+ <tr v-if="(item as any).property.minimum != null">
78
+ <td class="constraint-key">Minimum</td>
79
+ <td>{{ (item as any).property.minimum }}</td>
80
+ </tr>
81
+ <tr v-if="(item as any).property.maximum != null">
82
+ <td class="constraint-key">Maximum</td>
83
+ <td>{{ (item as any).property.maximum }}</td>
84
+ </tr>
85
+ <tr v-if="(item as any).property.minLength != null">
86
+ <td class="constraint-key">Min Length</td>
87
+ <td>{{ (item as any).property.minLength }}</td>
88
+ </tr>
89
+ <tr v-if="(item as any).property.maxLength != null">
90
+ <td class="constraint-key">Max Length</td>
91
+ <td>{{ (item as any).property.maxLength }}</td>
92
+ </tr>
93
+ <tr v-if="(item as any).property.pattern">
94
+ <td class="constraint-key">Pattern</td>
95
+ <td class="font-mono">{{ (item as any).property.pattern }}</td>
96
+ </tr>
97
+ <tr v-if="(item as any).property.enum?.length">
98
+ <td class="constraint-key">Enum</td>
99
+ <td>{{ (item as any).property.enum.join(', ') }}</td>
100
+ </tr>
101
+ <tr v-if="(item as any).property.itemsType">
102
+ <td class="constraint-key">Items</td>
103
+ <td>{{ (item as any).property.itemsType }}</td>
104
+ </tr>
105
+ </tbody>
106
+ </table>
107
+ </div>
108
+ </template>
109
+
110
+ <!-- Properties/Definition tab -->
111
+ <template v-if="uiStore.activePanelTab === 'properties'">
112
+ <div class="detail-section">
113
+ <table v-if="properties.length" class="table">
114
+ <thead>
115
+ <tr>
116
+ <th>Name</th>
117
+ <th>Type</th>
118
+ <th>Required</th>
119
+ <th>Description</th>
120
+ </tr>
121
+ </thead>
122
+ <tbody>
123
+ <tr v-for="prop in properties" :key="prop.name">
124
+ <td>
125
+ <span class="font-mono">{{ prop.name }}</span>
126
+ <span v-if="prop.deprecated" class="badge badge-deprecated-sm">deprecated</span>
127
+ </td>
128
+ <td>
129
+ <span class="prop-type">{{ prop.type || 'any' }}</span>
130
+ <span v-if="prop.format" class="prop-format">{{ prop.format }}</span>
131
+ <span v-if="prop.itemsType" class="prop-format">[{{ prop.itemsType }}]</span>
132
+ </td>
133
+ <td>
134
+ <span v-if="prop.required" class="badge badge-required-sm">yes</span>
135
+ <span v-else class="text-muted">no</span>
136
+ </td>
137
+ <td class="text-secondary">{{ prop.description || '—' }}</td>
138
+ </tr>
139
+ </tbody>
140
+ </table>
141
+ <p v-else class="text-muted">No properties defined.</p>
142
+ </div>
143
+ </template>
144
+ </template>
145
+
146
+ <div v-else class="panel-empty">
147
+ <p>Select an item to view details</p>
148
+ </div>
149
+ </div>
150
+ </aside>
151
+ </div>
152
+ </template>
153
+
154
+ <script setup lang="ts">
155
+ import { computed } from 'vue'
156
+ import { useSchemaStore, type SelectedItem } from '../stores/schemaStore'
157
+ import { useUiStore } from '../stores/uiStore'
158
+ import type { SpaProperty } from '../types'
159
+
160
+ const schemaStore = useSchemaStore()
161
+ const uiStore = useUiStore()
162
+
163
+ const item = computed<SelectedItem | null>(() => schemaStore.selectedItem)
164
+ const schema = computed(() => schemaStore.selectedSchema)
165
+
166
+ const itemTitle = computed(() => {
167
+ if (!item.value) return ''
168
+ switch (item.value.kind) {
169
+ case 'schema': return item.value.schema.title || item.value.schema.name
170
+ case 'definition': return item.value.definition.title || item.value.definition.name
171
+ case 'property': return item.value.property.name
172
+ }
173
+ })
174
+
175
+ const itemType = computed(() => {
176
+ if (!item.value) return ''
177
+ switch (item.value.kind) {
178
+ case 'schema': return item.value.schema.type || 'any'
179
+ case 'definition': return item.value.definition.type || 'any'
180
+ case 'property': return item.value.property.type || 'any'
181
+ }
182
+ })
183
+
184
+ const itemDescription = computed(() => {
185
+ if (!item.value) return ''
186
+ switch (item.value.kind) {
187
+ case 'schema': return item.value.schema.description
188
+ case 'definition': return item.value.definition.description
189
+ case 'property': return item.value.property.description
190
+ }
191
+ })
192
+
193
+ const properties = computed<SpaProperty[]>(() => {
194
+ if (!item.value) return []
195
+ switch (item.value.kind) {
196
+ case 'schema': return item.value.schema.properties
197
+ case 'definition': return item.value.definition.properties
198
+ case 'property': return []
199
+ }
200
+ })
201
+
202
+ const hasConstraints = computed(() => {
203
+ if (item.value?.kind !== 'property') return false
204
+ const p = item.value.property
205
+ return p.minimum != null || p.maximum != null ||
206
+ p.minLength != null || p.maxLength != null ||
207
+ p.pattern || p.enum?.length || p.itemsType
208
+ })
209
+
210
+ type TabId = 'overview' | 'properties'
211
+
212
+ const currentTabs = computed<{ id: TabId; label: string }[]>(() => {
213
+ const tabs: { id: TabId; label: string }[] = [
214
+ { id: 'overview', label: 'Overview' },
215
+ ]
216
+ if (properties.value.length > 0) {
217
+ tabs.push({ id: 'properties', label: `Properties (${properties.value.length})` })
218
+ }
219
+ return tabs
220
+ })
221
+ </script>
222
+
223
+ <style scoped>
224
+ .detail-panel-overlay {
225
+ position: fixed;
226
+ inset: 0;
227
+ background: var(--bg-overlay);
228
+ display: flex;
229
+ justify-content: flex-end;
230
+ z-index: 100;
231
+ animation: fadeIn var(--transition-fast);
232
+ }
233
+
234
+ .detail-panel {
235
+ width: 100%;
236
+ max-width: 640px;
237
+ height: 100%;
238
+ background: var(--bg-elevated);
239
+ box-shadow: var(--shadow-lg);
240
+ display: flex;
241
+ flex-direction: column;
242
+ animation: slideIn var(--transition-slow);
243
+ }
244
+
245
+ .panel-header {
246
+ display: flex;
247
+ align-items: center;
248
+ justify-content: space-between;
249
+ padding: var(--space-4) var(--space-5);
250
+ border-bottom: 1px solid var(--border-light);
251
+ flex-shrink: 0;
252
+ }
253
+
254
+ .panel-title h2 {
255
+ font-size: var(--text-lg);
256
+ font-weight: 600;
257
+ white-space: nowrap;
258
+ overflow: hidden;
259
+ text-overflow: ellipsis;
260
+ }
261
+
262
+ .panel-tabs {
263
+ display: flex;
264
+ gap: var(--space-1);
265
+ padding: var(--space-2) var(--space-5);
266
+ border-bottom: 1px solid var(--border-light);
267
+ flex-shrink: 0;
268
+ }
269
+
270
+ .panel-tab {
271
+ padding: var(--space-2) var(--space-3);
272
+ font-size: var(--text-sm);
273
+ font-weight: 500;
274
+ color: var(--text-muted);
275
+ border-radius: var(--radius-md);
276
+ transition: all var(--transition-fast);
277
+ }
278
+
279
+ .panel-tab:hover {
280
+ color: var(--text-primary);
281
+ background: var(--bg-hover);
282
+ }
283
+
284
+ .panel-tab.active {
285
+ color: var(--color-primary);
286
+ background: var(--color-primary-alpha);
287
+ }
288
+
289
+ .panel-content {
290
+ flex: 1;
291
+ overflow-y: auto;
292
+ padding: var(--space-5);
293
+ }
294
+
295
+ .panel-empty {
296
+ display: flex;
297
+ align-items: center;
298
+ justify-content: center;
299
+ height: 100%;
300
+ color: var(--text-muted);
301
+ font-size: var(--text-sm);
302
+ }
303
+
304
+ .detail-section {
305
+ margin-bottom: var(--space-6);
306
+ }
307
+
308
+ .detail-heading {
309
+ font-size: var(--text-base);
310
+ font-weight: 600;
311
+ margin-bottom: var(--space-3);
312
+ color: var(--text-primary);
313
+ }
314
+
315
+ .detail-meta {
316
+ display: flex;
317
+ flex-direction: column;
318
+ gap: var(--space-3);
319
+ }
320
+
321
+ .meta-row {
322
+ display: flex;
323
+ align-items: flex-start;
324
+ gap: var(--space-3);
325
+ }
326
+
327
+ .meta-label {
328
+ font-size: var(--text-sm);
329
+ font-weight: 500;
330
+ color: var(--text-muted);
331
+ min-width: 80px;
332
+ flex-shrink: 0;
333
+ }
334
+
335
+ .meta-tags {
336
+ display: flex;
337
+ flex-wrap: wrap;
338
+ gap: var(--space-1);
339
+ }
340
+
341
+ .prop-type {
342
+ color: var(--color-primary);
343
+ font-weight: 500;
344
+ }
345
+
346
+ .prop-format {
347
+ font-size: var(--text-xs);
348
+ color: var(--text-muted);
349
+ margin-left: var(--space-1);
350
+ }
351
+
352
+ .badge-type {
353
+ background: var(--badge-schema-bg);
354
+ color: var(--badge-schema);
355
+ }
356
+
357
+ .badge-format {
358
+ background: var(--bg-secondary);
359
+ color: var(--text-secondary);
360
+ }
361
+
362
+ .badge-required {
363
+ background: var(--badge-required-bg);
364
+ color: var(--badge-required);
365
+ }
366
+
367
+ .badge-optional {
368
+ background: var(--bg-secondary);
369
+ color: var(--text-muted);
370
+ }
371
+
372
+ .badge-deprecated {
373
+ background: var(--badge-deprecated-bg);
374
+ color: var(--badge-deprecated);
375
+ }
376
+
377
+ .badge-required-sm {
378
+ background: var(--badge-required-bg);
379
+ color: var(--badge-required);
380
+ font-size: 10px;
381
+ padding: 1px 4px;
382
+ border-radius: 2px;
383
+ }
384
+
385
+ .badge-deprecated-sm {
386
+ background: var(--badge-deprecated-bg);
387
+ color: var(--badge-deprecated);
388
+ font-size: 10px;
389
+ padding: 1px 4px;
390
+ border-radius: 2px;
391
+ margin-left: 4px;
392
+ }
393
+
394
+ .constraint-table td:first-child {
395
+ font-weight: 500;
396
+ color: var(--text-secondary);
397
+ width: 120px;
398
+ }
399
+
400
+ .constraint-key {
401
+ white-space: nowrap;
402
+ }
403
+ </style>