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,118 @@
1
+ import { defineStore } from 'pinia'
2
+ import { ref, computed } from 'vue'
3
+ import type { SpaDocument, SpaSchema, SpaDefinition, SpaProperty } from '../types'
4
+
5
+ export type SelectedItem =
6
+ | { kind: 'schema'; schema: SpaSchema }
7
+ | { kind: 'definition'; schema: SpaSchema; definition: SpaDefinition }
8
+ | { kind: 'property'; schema: SpaSchema; property: SpaProperty }
9
+
10
+ export const useSchemaStore = defineStore('schema', () => {
11
+ const data = ref<SpaDocument | null>(null)
12
+ const selectedSchemaName = ref<string | null>(null)
13
+ const selectedItemKey = ref<string | null>(null)
14
+
15
+ const metadata = computed(() => data.value?.metadata)
16
+ const schemas = computed(() => data.value?.schemas ?? [])
17
+ const searchIndex = computed(() => data.value?.searchIndex ?? [])
18
+
19
+ const selectedSchema = computed<SpaSchema | undefined>(() =>
20
+ schemas.value.find(s => s.name === selectedSchemaName.value)
21
+ )
22
+
23
+ const selectedItem = computed<SelectedItem | null>(() => {
24
+ const schema = selectedSchema.value
25
+ if (!schema) return null
26
+ if (!selectedItemKey.value) return { kind: 'schema', schema }
27
+
28
+ if (selectedItemKey.value.startsWith('def:')) {
29
+ const name = selectedItemKey.value.slice(4)
30
+ const definition = schema.definitions.find(d => d.name === name)
31
+ if (definition) return { kind: 'definition', schema, definition }
32
+ }
33
+
34
+ if (selectedItemKey.value.startsWith('prop:')) {
35
+ const name = selectedItemKey.value.slice(5)
36
+ const property = schema.properties.find(p => p.name === name)
37
+ if (property) return { kind: 'property', schema, property }
38
+ }
39
+
40
+ return { kind: 'schema', schema }
41
+ })
42
+
43
+ const schemaCounts = computed(() => ({
44
+ schemas: schemas.value.length,
45
+ properties: schemas.value.reduce((acc, s) => acc + s.properties.length, 0),
46
+ definitions: schemas.value.reduce((acc, s) => acc + s.definitions.length, 0),
47
+ }))
48
+
49
+ function loadFromWindow() {
50
+ if (typeof window !== 'undefined' && window.SCHEMA_DATA) {
51
+ const raw = window.SCHEMA_DATA
52
+ // Normalize: ensure arrays are always arrays (backend may omit empty ones)
53
+ for (const schema of raw.schemas) {
54
+ schema.properties = schema.properties ?? []
55
+ schema.definitions = schema.definitions ?? []
56
+ schema.required = schema.required ?? []
57
+ // Normalize: JSON key is "$ref" (Ruby maps it that way) but TypeScript uses "ref"
58
+ for (const prop of schema.properties) {
59
+ if (prop.$ref !== undefined) {
60
+ prop.ref = prop.$ref
61
+ delete prop.$ref
62
+ }
63
+ }
64
+ for (const def of schema.definitions) {
65
+ def.properties = def.properties ?? []
66
+ def.required = def.required ?? []
67
+ for (const prop of def.properties) {
68
+ if (prop.$ref !== undefined) {
69
+ prop.ref = prop.$ref
70
+ delete prop.$ref
71
+ }
72
+ }
73
+ }
74
+ }
75
+ raw.searchIndex = raw.searchIndex ?? []
76
+ data.value = raw
77
+ }
78
+ }
79
+
80
+ function selectSchema(name: string | null) {
81
+ selectedSchemaName.value = name
82
+ selectedItemKey.value = null
83
+ }
84
+
85
+ function selectDefinition(name: string) {
86
+ selectedItemKey.value = `def:${name}`
87
+ }
88
+
89
+ function selectProperty(name: string) {
90
+ selectedItemKey.value = `prop:${name}`
91
+ }
92
+
93
+ function clearSelection() {
94
+ selectedItemKey.value = null
95
+ }
96
+
97
+ function schemaByName(name: string): SpaSchema | undefined {
98
+ return schemas.value.find(s => s.name === name)
99
+ }
100
+
101
+ return {
102
+ data,
103
+ selectedSchemaName,
104
+ selectedItemKey,
105
+ metadata,
106
+ schemas,
107
+ searchIndex,
108
+ selectedSchema,
109
+ selectedItem,
110
+ schemaCounts,
111
+ loadFromWindow,
112
+ selectSchema,
113
+ selectDefinition,
114
+ selectProperty,
115
+ clearSelection,
116
+ schemaByName,
117
+ }
118
+ })
@@ -0,0 +1,78 @@
1
+ import { defineStore } from 'pinia'
2
+ import { ref, computed } from 'vue'
3
+
4
+ export type Theme = 'light' | 'dark' | 'system'
5
+
6
+ export const useUiStore = defineStore('ui', () => {
7
+ const theme = ref<Theme>('system')
8
+ const resolvedTheme = ref<'light' | 'dark'>('light')
9
+ const sidebarCollapsed = ref(false)
10
+ const detailPanelOpen = ref(false)
11
+ const activePanelTab = ref<'overview' | 'definition'>('overview')
12
+ const searchOpen = ref(false)
13
+ const expandedSchemaNames = ref<Set<string>>(new Set())
14
+
15
+ const isDark = computed(() => resolvedTheme.value === 'dark')
16
+
17
+ function initTheme() {
18
+ const stored = localStorage.getItem('lutaml-jsonschema-theme') as Theme | null
19
+ if (stored) theme.value = stored
20
+ updateResolvedTheme()
21
+ }
22
+
23
+ function updateResolvedTheme() {
24
+ if (theme.value === 'system') {
25
+ resolvedTheme.value = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
26
+ } else {
27
+ resolvedTheme.value = theme.value
28
+ }
29
+ applyTheme()
30
+ }
31
+
32
+ function applyTheme() {
33
+ if (resolvedTheme.value === 'dark') {
34
+ document.documentElement.setAttribute('data-theme', 'dark')
35
+ } else {
36
+ document.documentElement.removeAttribute('data-theme')
37
+ }
38
+ }
39
+
40
+ function toggleTheme() {
41
+ const next = resolvedTheme.value === 'light' ? 'dark' : 'light'
42
+ theme.value = next
43
+ localStorage.setItem('lutaml-jsonschema-theme', next)
44
+ updateResolvedTheme()
45
+ }
46
+
47
+ function toggleSidebar() { sidebarCollapsed.value = !sidebarCollapsed.value }
48
+ function openDetailPanel() { detailPanelOpen.value = true }
49
+ function closeDetailPanel() { detailPanelOpen.value = false }
50
+ function setPanelTab(tab: 'overview' | 'definition') { activePanelTab.value = tab }
51
+ function openSearch() { searchOpen.value = true }
52
+ function closeSearch() { searchOpen.value = false }
53
+
54
+ function toggleSchemaExpanded(name: string) {
55
+ if (expandedSchemaNames.value.has(name)) {
56
+ expandedSchemaNames.value.delete(name)
57
+ } else {
58
+ expandedSchemaNames.value.add(name)
59
+ }
60
+ }
61
+
62
+ function isSchemaExpanded(name: string): boolean {
63
+ return expandedSchemaNames.value.has(name)
64
+ }
65
+
66
+ if (typeof window !== 'undefined') {
67
+ window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
68
+ if (theme.value === 'system') updateResolvedTheme()
69
+ })
70
+ }
71
+
72
+ return {
73
+ theme, resolvedTheme, sidebarCollapsed, detailPanelOpen, activePanelTab,
74
+ searchOpen, expandedSchemaNames, isDark,
75
+ initTheme, toggleTheme, toggleSidebar, openDetailPanel, closeDetailPanel,
76
+ setPanelTab, openSearch, closeSearch, toggleSchemaExpanded, isSchemaExpanded,
77
+ }
78
+ })
@@ -0,0 +1,194 @@
1
+ /* Lutaml-JSONSchema SPA - LutaML Branded Design */
2
+
3
+ :root {
4
+ --color-primary: #5b9cd4;
5
+ --color-primary-light: #7db5e2;
6
+ --color-primary-dark: #26489e;
7
+ --color-primary-alpha: rgba(91, 156, 212, 0.12);
8
+
9
+ --color-accent: #dbab3e;
10
+ --color-accent-light: #f0ca6e;
11
+ --color-accent-alpha: rgba(219, 171, 62, 0.12);
12
+
13
+ --color-teal: #60c3a7;
14
+ --color-teal-alpha: rgba(96, 195, 167, 0.12);
15
+ --color-orange: #ea5624;
16
+ --color-orange-alpha: rgba(234, 86, 36, 0.12);
17
+ --color-red: #b31f24;
18
+ --color-red-alpha: rgba(179, 31, 36, 0.12);
19
+ --color-green: #7cc242;
20
+ --color-green-alpha: rgba(124, 194, 66, 0.12);
21
+
22
+ --bg-primary: #FAFAF9;
23
+ --bg-secondary: #F5F5F4;
24
+ --bg-elevated: #FFFFFF;
25
+ --bg-overlay: rgba(28, 25, 23, 0.5);
26
+ --bg-hover: rgba(28, 25, 23, 0.04);
27
+ --bg-active: rgba(28, 25, 23, 0.06);
28
+
29
+ --text-primary: #1C1917;
30
+ --text-secondary: #57534E;
31
+ --text-muted: #A8A29E;
32
+
33
+ --border-light: #E7E5E4;
34
+ --border-medium: #D6D3D1;
35
+ --border-focus: #5b9cd4;
36
+
37
+ --badge-schema: #26489e;
38
+ --badge-schema-bg: rgba(38, 72, 158, 0.12);
39
+ --badge-property: #60c3a7;
40
+ --badge-property-bg: rgba(96, 195, 167, 0.15);
41
+ --badge-definition: #dbab3e;
42
+ --badge-definition-bg: rgba(219, 171, 62, 0.15);
43
+ --badge-required: #5b9cd4;
44
+ --badge-required-bg: rgba(91, 156, 212, 0.12);
45
+ --badge-deprecated: #b31f24;
46
+ --badge-deprecated-bg: rgba(179, 31, 36, 0.12);
47
+
48
+ --shadow-sm: 0 1px 2px rgba(28, 25, 23, 0.05);
49
+ --shadow-md: 0 4px 6px -1px rgba(28, 25, 23, 0.07), 0 2px 4px -1px rgba(28, 25, 23, 0.04);
50
+ --shadow-lg: 0 10px 15px -3px rgba(28, 25, 23, 0.08), 0 4px 6px -2px rgba(28, 25, 23, 0.04);
51
+
52
+ --font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
53
+ --font-mono: 'JetBrains Mono', 'SF Mono', Monaco, 'Consolas', monospace;
54
+
55
+ --text-xs: 0.75rem;
56
+ --text-sm: 0.875rem;
57
+ --text-base: 1rem;
58
+ --text-lg: 1.125rem;
59
+ --text-xl: 1.25rem;
60
+ --text-2xl: 1.5rem;
61
+
62
+ --leading-tight: 1.25;
63
+ --leading-normal: 1.5;
64
+ --leading-relaxed: 1.75;
65
+
66
+ --space-1: 0.25rem;
67
+ --space-2: 0.5rem;
68
+ --space-3: 0.75rem;
69
+ --space-4: 1rem;
70
+ --space-5: 1.25rem;
71
+ --space-6: 1.5rem;
72
+ --space-8: 2rem;
73
+ --space-10: 2.5rem;
74
+ --space-12: 3rem;
75
+
76
+ --radius-sm: 0.25rem;
77
+ --radius-md: 0.375rem;
78
+ --radius-lg: 0.5rem;
79
+ --radius-xl: 0.75rem;
80
+
81
+ --transition-fast: 100ms ease;
82
+ --transition-normal: 150ms ease;
83
+ --transition-slow: 200ms ease-out;
84
+ }
85
+
86
+ :root[data-theme="dark"] {
87
+ --bg-primary: #1a1a24;
88
+ --bg-secondary: #232736;
89
+ --bg-elevated: #2d3344;
90
+ --bg-overlay: rgba(0, 0, 0, 0.7);
91
+ --bg-hover: rgba(255, 255, 255, 0.06);
92
+ --bg-active: rgba(255, 255, 255, 0.10);
93
+
94
+ --text-primary: #e8edf4;
95
+ --text-secondary: #a8b4c8;
96
+ --text-muted: #6b7a8f;
97
+
98
+ --border-light: #3a4256;
99
+ --border-medium: #4a5572;
100
+ --border-focus: #5b9cd4;
101
+
102
+ --badge-schema: #5b9cd4;
103
+ --badge-schema-bg: rgba(91, 156, 212, 0.2);
104
+ --badge-property: #60c3a7;
105
+ --badge-property-bg: rgba(96, 195, 167, 0.2);
106
+ --badge-definition: #dbab3e;
107
+ --badge-definition-bg: rgba(219, 171, 62, 0.2);
108
+ --badge-required: #5b9cd4;
109
+ --badge-required-bg: rgba(91, 156, 212, 0.2);
110
+ --badge-deprecated: #b31f24;
111
+ --badge-deprecated-bg: rgba(179, 31, 36, 0.2);
112
+
113
+ --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.4);
114
+ --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.5), 0 2px 4px -1px rgba(0, 0, 0, 0.3);
115
+ --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.6), 0 4px 6px -2px rgba(0, 0, 0, 0.4);
116
+ }
117
+
118
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
119
+
120
+ html {
121
+ font-size: 16px;
122
+ -webkit-font-smoothing: antialiased;
123
+ -moz-osx-font-smoothing: grayscale;
124
+ }
125
+
126
+ body {
127
+ font-family: var(--font-sans);
128
+ font-size: var(--text-base);
129
+ line-height: var(--leading-normal);
130
+ color: var(--text-primary);
131
+ background-color: var(--bg-primary);
132
+ }
133
+
134
+ h1, h2, h3, h4, h5, h6 {
135
+ font-weight: 600;
136
+ line-height: var(--leading-tight);
137
+ color: var(--text-primary);
138
+ }
139
+ h1 { font-size: var(--text-2xl); }
140
+ h2 { font-size: var(--text-xl); }
141
+ h3 { font-size: var(--text-lg); }
142
+
143
+ code, pre { font-family: var(--font-mono); font-size: 0.9em; }
144
+ code { background: var(--bg-secondary); padding: 0.125em 0.375em; border-radius: var(--radius-sm); }
145
+ pre { background: var(--bg-secondary); padding: var(--space-4); border-radius: var(--radius-md); overflow-x: auto; }
146
+ pre code { background: none; padding: 0; display: block; }
147
+
148
+ a { color: var(--color-primary); text-decoration: none; transition: color var(--transition-fast); }
149
+ a:hover { color: var(--color-primary-dark); text-decoration: underline; }
150
+
151
+ button { font-family: inherit; font-size: inherit; cursor: pointer; border: none; background: none; }
152
+
153
+ .btn {
154
+ display: inline-flex; align-items: center; justify-content: center;
155
+ gap: var(--space-2); padding: var(--space-2) var(--space-4);
156
+ font-size: var(--text-sm); font-weight: 500; border-radius: var(--radius-md);
157
+ transition: all var(--transition-fast);
158
+ }
159
+ .btn-primary { background: var(--color-primary); color: white; }
160
+ .btn-primary:hover { background: var(--color-primary-dark); }
161
+ .btn-secondary { background: var(--bg-secondary); color: var(--text-primary); border: 1px solid var(--border-medium); }
162
+ .btn-secondary:hover { background: var(--bg-hover); }
163
+ .btn-ghost { color: var(--text-secondary); }
164
+ .btn-ghost:hover { background: var(--bg-hover); color: var(--text-primary); }
165
+
166
+ .badge {
167
+ display: inline-flex; align-items: center; padding: 0.125rem 0.5rem;
168
+ font-size: var(--text-xs); font-weight: 500; border-radius: var(--radius-sm);
169
+ text-transform: uppercase; letter-spacing: 0.025em;
170
+ }
171
+
172
+ .card {
173
+ background: var(--bg-elevated); border: 1px solid var(--border-light);
174
+ border-radius: var(--radius-lg); box-shadow: var(--shadow-sm);
175
+ }
176
+
177
+ .table { width: 100%; border-collapse: collapse; font-size: var(--text-sm); }
178
+ .table th, .table td { padding: var(--space-2) var(--space-3); text-align: left; border-bottom: 1px solid var(--border-light); }
179
+ .table th { font-weight: 500; color: var(--text-secondary); background: var(--bg-secondary); }
180
+ .table tr:hover td { background: var(--bg-hover); }
181
+
182
+ ::-webkit-scrollbar { width: 8px; height: 8px; }
183
+ ::-webkit-scrollbar-track { background: transparent; }
184
+ ::-webkit-scrollbar-thumb { background: var(--border-medium); border-radius: 4px; }
185
+ ::-webkit-scrollbar-thumb:hover { background: var(--text-muted); }
186
+
187
+ @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
188
+ @keyframes slideIn { from { opacity: 0; transform: translateX(10px); } to { opacity: 1; transform: translateX(0); } }
189
+
190
+ .text-muted { color: var(--text-muted); }
191
+ .text-secondary { color: var(--text-secondary); }
192
+ .font-mono { font-family: var(--font-mono); }
193
+ .truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
194
+ .sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; }
@@ -0,0 +1,70 @@
1
+ export interface SpaMetadata {
2
+ title?: string
3
+ version?: string
4
+ description?: string
5
+ baseUrl?: string
6
+ theme?: string
7
+ }
8
+
9
+ export interface SpaProperty {
10
+ name: string
11
+ title?: string
12
+ description?: string
13
+ type?: string
14
+ format?: string
15
+ required?: boolean
16
+ default?: string
17
+ pattern?: string
18
+ enum?: string[]
19
+ /** Resolved $ref — normalized from backend's "$ref" key during loadFromWindow() */
20
+ ref?: string
21
+ /** Raw JSON Schema $ref — present in window.SCHEMA_DATA before normalization */
22
+ $ref?: string
23
+ minLength?: number
24
+ maxLength?: number
25
+ minimum?: number
26
+ maximum?: number
27
+ itemsType?: string
28
+ deprecated?: boolean
29
+ examples?: string[]
30
+ }
31
+
32
+ export interface SpaDefinition {
33
+ name: string
34
+ title?: string
35
+ description?: string
36
+ type?: string
37
+ properties: SpaProperty[]
38
+ required: string[]
39
+ examples?: string[]
40
+ }
41
+
42
+ export interface SpaSchema {
43
+ name: string
44
+ title?: string
45
+ description?: string
46
+ type?: string
47
+ properties: SpaProperty[]
48
+ definitions: SpaDefinition[]
49
+ required: string[]
50
+ examples?: string[]
51
+ }
52
+
53
+ export interface SpaSearchEntry {
54
+ name: string
55
+ title?: string
56
+ type: string
57
+ schemaName: string
58
+ }
59
+
60
+ export interface SpaDocument {
61
+ metadata: SpaMetadata
62
+ schemas: SpaSchema[]
63
+ searchIndex: SpaSearchEntry[]
64
+ }
65
+
66
+ declare global {
67
+ interface Window {
68
+ SCHEMA_DATA: SpaDocument
69
+ }
70
+ }