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,396 @@
1
+ <template>
2
+ <div class="home-view">
3
+ <!-- Schema Detail Mode -->
4
+ <div v-if="schemaStore.selectedSchema" class="selected-schema">
5
+ <div class="schema-header">
6
+ <div>
7
+ <h1>{{ schemaStore.selectedSchema.title || schemaStore.selectedSchema.name }}</h1>
8
+ <p v-if="schemaStore.selectedSchema.description" class="schema-desc text-secondary">{{ schemaStore.selectedSchema.description }}</p>
9
+ <div class="schema-meta-row">
10
+ <span class="badge badge-type">{{ schemaStore.selectedSchema.type || 'any' }}</span>
11
+ <span class="text-muted">{{ schemaStore.selectedSchema.properties.length }} properties</span>
12
+ <span v-if="schemaStore.selectedSchema.required.length" class="text-muted">&middot; {{ schemaStore.selectedSchema.required.length }} required</span>
13
+ <span v-if="schemaStore.selectedSchema.definitions.length" class="text-muted">&middot; {{ schemaStore.selectedSchema.definitions.length }} definitions</span>
14
+ </div>
15
+ </div>
16
+ </div>
17
+
18
+ <div class="schema-tabs">
19
+ <button
20
+ v-for="tab in tabs"
21
+ :key="tab.id"
22
+ class="tab-btn"
23
+ :class="{ active: activeTab === tab.id }"
24
+ @click="activeTab = tab.id"
25
+ >
26
+ {{ tab.label }}
27
+ <span class="tab-count">{{ tab.count }}</span>
28
+ </button>
29
+ </div>
30
+
31
+ <div class="tab-content">
32
+ <!-- Builder Tab -->
33
+ <div v-if="activeTab === 'builder'" class="tab-pane">
34
+ <SchemaBuilder
35
+ :properties="schemaStore.selectedSchema.properties"
36
+ :required="schemaStore.selectedSchema.required"
37
+ :schema="schemaStore.selectedSchema"
38
+ />
39
+ </div>
40
+
41
+ <!-- Definitions Tab -->
42
+ <div v-if="activeTab === 'definitions'" class="tab-pane">
43
+ <div v-if="schemaStore.selectedSchema.definitions.length" class="def-list">
44
+ <div
45
+ v-for="def in schemaStore.selectedSchema.definitions"
46
+ :key="def.name"
47
+ class="def-card card"
48
+ >
49
+ <div class="def-card-header" @click="toggleDef(def.name)">
50
+ <span class="def-chevron" :class="{ expanded: expandedDefs.has(def.name) }">&#9654;</span>
51
+ <span class="def-card-title">{{ def.title || def.name }}</span>
52
+ <span class="def-card-name font-mono text-muted">{{ def.name }}</span>
53
+ <span v-if="def.type" class="def-type-badge">{{ def.type }}</span>
54
+ <span class="text-muted">{{ def.properties.length }} props</span>
55
+ <span v-if="def.required?.length" class="text-muted">&middot; {{ def.required.length }} required</span>
56
+ </div>
57
+ <p v-if="def.description" class="def-card-desc text-secondary">{{ def.description }}</p>
58
+ <div v-if="expandedDefs.has(def.name)" class="def-card-body">
59
+ <SchemaBuilder
60
+ :properties="def.properties"
61
+ :required="def.required"
62
+ :schema="schemaStore.selectedSchema"
63
+ />
64
+ </div>
65
+ </div>
66
+ </div>
67
+ <div v-else class="empty-state">
68
+ <p class="text-muted">No definitions.</p>
69
+ </div>
70
+ </div>
71
+ </div>
72
+ </div>
73
+
74
+ <!-- Landing Page -->
75
+ <div v-else class="landing-page">
76
+ <div class="landing-header">
77
+ <h1>{{ schemaStore.metadata?.title || 'JSON Schema Documentation' }}</h1>
78
+ <p v-if="schemaStore.metadata?.description" class="landing-description">{{ schemaStore.metadata.description }}</p>
79
+ <div class="landing-subtitle">
80
+ <span>{{ schemaStore.schemaCounts.schemas }} schemas</span>
81
+ <span class="separator">&middot;</span>
82
+ <span>{{ schemaStore.schemaCounts.properties }} properties</span>
83
+ <span class="separator">&middot;</span>
84
+ <span>{{ schemaStore.schemaCounts.definitions }} definitions</span>
85
+ </div>
86
+ </div>
87
+
88
+ <div class="schema-grid">
89
+ <div
90
+ v-for="schema in schemaStore.schemas"
91
+ :key="schema.name"
92
+ class="schema-card card"
93
+ @click="selectSchema(schema.name)"
94
+ >
95
+ <div class="schema-card-icon">
96
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none">
97
+ <rect x="3" y="3" width="18" height="18" rx="2" stroke="currentColor" stroke-width="1.5"/>
98
+ <path d="M7 8h10M7 12h6M7 16h8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
99
+ </svg>
100
+ </div>
101
+ <div class="schema-card-content">
102
+ <h3>{{ schema.title || schema.name }}</h3>
103
+ <p v-if="schema.description" class="schema-card-desc text-secondary">{{ schema.description }}</p>
104
+ <div class="schema-card-meta">
105
+ <span class="badge badge-type-sm">{{ schema.type || 'any' }}</span>
106
+ </div>
107
+ <div class="schema-card-stats">
108
+ <span>{{ schema.properties.length }} properties</span>
109
+ <span>{{ schema.definitions.length }} definitions</span>
110
+ <span v-if="schema.required.length">{{ schema.required.length }} required</span>
111
+ </div>
112
+ </div>
113
+ </div>
114
+ </div>
115
+ </div>
116
+ </div>
117
+ </template>
118
+
119
+ <script setup lang="ts">
120
+ import { ref, computed, reactive } from 'vue'
121
+ import { useSchemaStore } from '../stores/schemaStore'
122
+ import SchemaBuilder from '../components/SchemaBuilder.vue'
123
+
124
+ type TabId = 'builder' | 'definitions'
125
+
126
+ const schemaStore = useSchemaStore()
127
+ const activeTab = ref<TabId>('builder')
128
+ const expandedDefs = reactive(new Set<string>())
129
+
130
+ const tabs = computed(() => {
131
+ const s = schemaStore.selectedSchema
132
+ if (!s) return []
133
+ return [
134
+ { id: 'builder' as TabId, label: 'Builder', count: s.properties.length },
135
+ { id: 'definitions' as TabId, label: 'Definitions', count: s.definitions.length },
136
+ ]
137
+ })
138
+
139
+ function selectSchema(name: string) {
140
+ schemaStore.selectSchema(name)
141
+ activeTab.value = 'builder'
142
+ }
143
+
144
+ function toggleDef(name: string) {
145
+ if (expandedDefs.has(name)) {
146
+ expandedDefs.delete(name)
147
+ } else {
148
+ expandedDefs.add(name)
149
+ }
150
+ }
151
+ </script>
152
+
153
+ <style scoped>
154
+ .home-view {
155
+ max-width: 1400px;
156
+ margin: 0 auto;
157
+ }
158
+
159
+ .schema-header {
160
+ display: flex;
161
+ align-items: flex-start;
162
+ justify-content: space-between;
163
+ margin-bottom: var(--space-6);
164
+ gap: var(--space-4);
165
+ }
166
+
167
+ .schema-header h1 {
168
+ font-size: var(--text-2xl);
169
+ margin-bottom: var(--space-1);
170
+ }
171
+
172
+ .schema-desc {
173
+ font-size: var(--text-base);
174
+ margin-bottom: var(--space-2);
175
+ line-height: var(--leading-relaxed);
176
+ }
177
+
178
+ .schema-meta-row {
179
+ display: flex;
180
+ align-items: center;
181
+ gap: var(--space-2);
182
+ margin-top: var(--space-2);
183
+ }
184
+
185
+ .schema-tabs {
186
+ display: flex;
187
+ gap: var(--space-1);
188
+ margin-bottom: var(--space-4);
189
+ border-bottom: 1px solid var(--border-light);
190
+ padding-bottom: var(--space-2);
191
+ }
192
+
193
+ .tab-btn {
194
+ display: flex;
195
+ align-items: center;
196
+ gap: var(--space-2);
197
+ padding: var(--space-2) var(--space-3);
198
+ font-size: var(--text-sm);
199
+ font-weight: 500;
200
+ color: var(--text-muted);
201
+ border-radius: var(--radius-md);
202
+ transition: all var(--transition-fast);
203
+ }
204
+
205
+ .tab-btn:hover {
206
+ color: var(--text-primary);
207
+ background: var(--bg-hover);
208
+ }
209
+
210
+ .tab-btn.active {
211
+ color: var(--color-primary);
212
+ background: var(--color-primary-alpha);
213
+ }
214
+
215
+ .tab-count {
216
+ font-size: var(--text-xs);
217
+ background: var(--bg-secondary);
218
+ padding: 1px 6px;
219
+ border-radius: var(--radius-sm);
220
+ }
221
+
222
+ /* Definitions */
223
+ .def-list {
224
+ display: flex;
225
+ flex-direction: column;
226
+ gap: var(--space-3);
227
+ }
228
+
229
+ .def-card {
230
+ padding: 0;
231
+ overflow: hidden;
232
+ }
233
+
234
+ .def-card-header {
235
+ display: flex;
236
+ align-items: center;
237
+ gap: var(--space-2);
238
+ padding: var(--space-3) var(--space-4);
239
+ cursor: pointer;
240
+ transition: background var(--transition-fast);
241
+ }
242
+
243
+ .def-card-header:hover {
244
+ background: var(--bg-hover);
245
+ }
246
+
247
+ .def-chevron {
248
+ font-size: 10px;
249
+ color: var(--text-muted);
250
+ transition: transform var(--transition-fast);
251
+ flex-shrink: 0;
252
+ }
253
+
254
+ .def-chevron.expanded {
255
+ transform: rotate(90deg);
256
+ }
257
+
258
+ .def-card-title {
259
+ font-weight: 600;
260
+ font-size: var(--text-sm);
261
+ }
262
+
263
+ .def-card-name {
264
+ font-size: var(--text-xs);
265
+ }
266
+
267
+ .def-type-badge {
268
+ font-size: 11px;
269
+ background: var(--badge-schema-bg);
270
+ color: var(--badge-schema);
271
+ padding: 1px 5px;
272
+ border-radius: var(--radius-sm);
273
+ }
274
+
275
+ .def-card-desc {
276
+ font-size: var(--text-sm);
277
+ padding: 0 var(--space-4) var(--space-2);
278
+ margin-top: calc(var(--space-1) * -1);
279
+ }
280
+
281
+ .def-card-body {
282
+ border-top: 1px solid var(--border-light);
283
+ padding: var(--space-4);
284
+ background: var(--bg-primary);
285
+ }
286
+
287
+ .badge-type {
288
+ background: var(--badge-schema-bg);
289
+ color: var(--badge-schema);
290
+ }
291
+
292
+ .badge-type-sm {
293
+ background: var(--bg-secondary);
294
+ color: var(--text-secondary);
295
+ font-size: 10px;
296
+ }
297
+
298
+ /* Landing page */
299
+ .landing-page {
300
+ padding: var(--space-6);
301
+ max-width: 1200px;
302
+ margin: 0 auto;
303
+ }
304
+
305
+ .landing-header {
306
+ margin-bottom: var(--space-8);
307
+ }
308
+
309
+ .landing-header h1 {
310
+ font-size: var(--text-2xl);
311
+ margin-bottom: var(--space-2);
312
+ }
313
+
314
+ .landing-description {
315
+ font-size: var(--text-base);
316
+ color: var(--text-secondary);
317
+ line-height: var(--leading-relaxed);
318
+ margin-bottom: var(--space-3);
319
+ max-width: 700px;
320
+ }
321
+
322
+ .landing-subtitle {
323
+ color: var(--text-muted);
324
+ font-size: var(--text-sm);
325
+ margin-bottom: var(--space-3);
326
+ }
327
+
328
+ .separator {
329
+ margin: 0 var(--space-1);
330
+ }
331
+
332
+ .schema-grid {
333
+ display: grid;
334
+ grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
335
+ gap: var(--space-4);
336
+ }
337
+
338
+ .schema-card {
339
+ display: flex;
340
+ align-items: flex-start;
341
+ gap: var(--space-3);
342
+ padding: var(--space-4);
343
+ cursor: pointer;
344
+ transition: all var(--transition-fast);
345
+ }
346
+
347
+ .schema-card:hover {
348
+ border-color: var(--color-primary);
349
+ box-shadow: var(--shadow-md);
350
+ }
351
+
352
+ .schema-card-icon {
353
+ color: var(--color-primary);
354
+ flex-shrink: 0;
355
+ margin-top: 2px;
356
+ }
357
+
358
+ .schema-card-content {
359
+ flex: 1;
360
+ min-width: 0;
361
+ }
362
+
363
+ .schema-card-content h3 {
364
+ font-size: var(--text-base);
365
+ font-weight: 600;
366
+ margin-bottom: var(--space-1);
367
+ white-space: nowrap;
368
+ overflow: hidden;
369
+ text-overflow: ellipsis;
370
+ }
371
+
372
+ .schema-card-desc {
373
+ font-size: var(--text-sm);
374
+ margin-bottom: var(--space-2);
375
+ display: -webkit-box;
376
+ -webkit-line-clamp: 2;
377
+ -webkit-box-orient: vertical;
378
+ overflow: hidden;
379
+ }
380
+
381
+ .schema-card-meta {
382
+ margin-bottom: var(--space-2);
383
+ }
384
+
385
+ .schema-card-stats {
386
+ display: flex;
387
+ gap: var(--space-3);
388
+ font-size: var(--text-xs);
389
+ color: var(--text-muted);
390
+ }
391
+
392
+ .empty-state {
393
+ padding: var(--space-8);
394
+ text-align: center;
395
+ }
396
+ </style>
@@ -0,0 +1,20 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "strict": true,
7
+ "jsx": "preserve",
8
+ "resolveJsonModule": true,
9
+ "isolatedModules": true,
10
+ "esModuleInterop": true,
11
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
12
+ "skipLibCheck": true,
13
+ "noEmit": true,
14
+ "paths": {
15
+ "@/*": ["./src/*"]
16
+ }
17
+ },
18
+ "include": ["src/**/*.ts", "src/**/*.vue"],
19
+ "exclude": ["node_modules", "dist"]
20
+ }
@@ -0,0 +1,28 @@
1
+ import { defineConfig } from 'vite'
2
+ import vue from '@vitejs/plugin-vue'
3
+
4
+ export default defineConfig({
5
+ plugins: [vue()],
6
+ define: {
7
+ 'process.env.NODE_ENV': JSON.stringify('production'),
8
+ },
9
+ build: {
10
+ lib: {
11
+ entry: 'src/app.ts',
12
+ name: 'LutamlJsonSchemaDocs',
13
+ formats: ['iife'],
14
+ fileName: () => 'app.iife.js',
15
+ },
16
+ cssCodeSplit: false,
17
+ rollupOptions: {
18
+ output: {
19
+ assetFileNames: 'style.[ext]',
20
+ inlineDynamicImports: true,
21
+ },
22
+ },
23
+ minify: 'terser',
24
+ terserOptions: {
25
+ compress: { drop_console: true },
26
+ },
27
+ },
28
+ })
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Jsonschema
5
+ class Base < Lutaml::Model::Serializable
6
+ end
7
+
8
+ # Forward declaration for circular references (Schema ↔ PropertyEntry ↔ Link).
9
+ class Schema < Base; end
10
+ end
11
+ end
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "thor"
4
+ require "json"
5
+ require "lutaml/jsonschema"
6
+ require "lutaml/jsonschema/configuration"
7
+
8
+ module Lutaml
9
+ module Jsonschema
10
+ class Cli < Thor
11
+ desc "spa SCHEMA_FILES...", "Generate SPA documentation from JSON Schema files"
12
+ option :output, aliases: "-o", type: :string, default: "output",
13
+ desc: "Output directory"
14
+ option :config, aliases: "-c", type: :string,
15
+ desc: "Path to YAML configuration file"
16
+ option :title, type: :string, desc: "Documentation title"
17
+ option :theme, type: :string, default: "light", desc: "Theme (light/dark)"
18
+ def spa(*schema_files)
19
+ raise Error, "No schema files provided" if schema_files.empty?
20
+
21
+ config = load_config(options[:config])
22
+ config.output_path = options[:output] if options[:output]
23
+ config.title = options[:title] if options[:title]
24
+ config.theme = options[:theme] if options[:theme]
25
+
26
+ schema_set = SchemaSet.load_from_files(*schema_files)
27
+ generator = Spa::Generator.new(schema_set, config.output_path,
28
+ metadata: config.to_metadata)
29
+ generator.generate
30
+ puts "SPA documentation generated at #{config.output_path}/index.html"
31
+ rescue Errno::ENOENT => e
32
+ abort "Error: #{e.message}"
33
+ end
34
+
35
+ desc "combine SCHEMA_FILES...", "Combine multiple JSON Schema files into one"
36
+ option :output, aliases: "-o", type: :string, default: "-",
37
+ desc: "Output file path (use - for stdout)"
38
+ def combine(*schema_files)
39
+ raise Error, "No schema files provided" if schema_files.empty?
40
+
41
+ schema_set = SchemaSet.load_from_files(*schema_files)
42
+ combined = Combiner.new.combine(schema_set)
43
+ json = JSON.pretty_generate(JSON.parse(combined.to_json))
44
+
45
+ if options[:output] == "-"
46
+ $stdout.write(json)
47
+ else
48
+ File.write(options[:output], json)
49
+ puts "Combined schema written to #{options[:output]}"
50
+ end
51
+ rescue Errno::ENOENT => e
52
+ abort "Error: #{e.message}"
53
+ end
54
+
55
+ desc "validate SCHEMA_FILE", "Validate a JSON Schema file"
56
+ def validate(schema_file)
57
+ data = File.read(schema_file)
58
+ json = JSON.parse(data)
59
+ Schema.from_json(data)
60
+
61
+ errors = validate_structure(json)
62
+ if errors.empty?
63
+ puts "#{schema_file}: valid"
64
+ else
65
+ errors.each { |e| warn " #{e}" }
66
+ abort "#{schema_file}: #{errors.length} validation error(s)"
67
+ end
68
+ rescue JSON::ParserError => e
69
+ abort "Invalid JSON: #{e.message}"
70
+ rescue Errno::ENOENT => e
71
+ abort "Error: #{e.message}"
72
+ end
73
+
74
+ desc "version", "Show version"
75
+ def version
76
+ puts "lutaml-jsonschema #{VERSION}"
77
+ end
78
+
79
+ private
80
+
81
+ def load_config(path)
82
+ return Configuration.new unless path
83
+
84
+ Configuration.load_from_file(path)
85
+ rescue Errno::ENOENT
86
+ abort "Configuration file not found: #{path}"
87
+ end
88
+
89
+ def validate_structure(json)
90
+ errors = []
91
+ errors << "Missing $schema" unless json.key?("$schema")
92
+ errors << "Missing type" unless json.key?("type")
93
+
94
+ errors << "properties must be an object" if json.key?("properties") && !json["properties"].is_a?(Hash)
95
+
96
+ errors << "required must be an array" if json.key?("required") && !json["required"].is_a?(Array)
97
+
98
+ errors
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Jsonschema
5
+ class Combiner
6
+ def combine(schema_set)
7
+ combined = Schema.new
8
+
9
+ schema_set.schemas.each do |name, schema|
10
+ schema.definition_entries.each do |entry|
11
+ combined.definition_entries.push(entry)
12
+ end
13
+
14
+ ref_entry = PropertyEntry.new(
15
+ name: name,
16
+ schema: Schema.new(dollar_ref: "#/definitions/#{name}")
17
+ )
18
+ combined.property_entries.push(ref_entry)
19
+
20
+ combined.title ||= schema.title if schema.title
21
+ end
22
+
23
+ rewrite_refs!(combined)
24
+ combined
25
+ end
26
+
27
+ private
28
+
29
+ def rewrite_refs!(schema)
30
+ return unless schema
31
+
32
+ schema.dollar_ref = localize_ref(schema.dollar_ref) if schema.dollar_ref&.start_with?("#/")
33
+
34
+ schema.property_entries.each { |e| rewrite_refs!(e.schema) }
35
+ schema.definition_entries.each { |e| rewrite_refs!(e.schema) }
36
+ schema.pattern_property_entries.each { |e| rewrite_refs!(e.schema) }
37
+ rewrite_refs!(schema.items)
38
+ rewrite_refs!(schema.not_schema)
39
+ rewrite_refs!(schema.if_schema)
40
+ rewrite_refs!(schema.then_schema)
41
+ rewrite_refs!(schema.else_schema)
42
+ schema.all_of.each { |s| rewrite_refs!(s) }
43
+ schema.any_of.each { |s| rewrite_refs!(s) }
44
+ schema.one_of.each { |s| rewrite_refs!(s) }
45
+ schema.links.each do |l| rewrite_refs!(l.schema)
46
+ rewrite_refs!(l.target_schema) end
47
+ end
48
+
49
+ def localize_ref(ref)
50
+ ref
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+
5
+ module Lutaml
6
+ module Jsonschema
7
+ class Configuration
8
+ attr_accessor :title, :version, :description, :base_url, :theme,
9
+ :output_path
10
+
11
+ def initialize
12
+ @title = nil
13
+ @version = nil
14
+ @description = nil
15
+ @base_url = nil
16
+ @theme = "light"
17
+ @output_path = "output"
18
+ end
19
+
20
+ def self.load_from_file(path)
21
+ return new unless File.exist?(path)
22
+
23
+ data = YAML.safe_load_file(path)
24
+ config = new
25
+ return config unless data.is_a?(Hash)
26
+
27
+ config.title = data["title"] if data.key?("title")
28
+ config.version = data["version"] if data.key?("version")
29
+ config.description = data["description"] if data.key?("description")
30
+ config.base_url = data["base_url"] if data.key?("base_url")
31
+ config.theme = data["theme"] if data.key?("theme")
32
+ config.output_path = data["output_path"] if data.key?("output_path")
33
+ config
34
+ end
35
+
36
+ def to_metadata
37
+ Spa::Metadata.new(
38
+ title: title,
39
+ version: version,
40
+ description: description,
41
+ base_url: base_url,
42
+ theme: theme
43
+ )
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Jsonschema
5
+ class Link < Base
6
+ attribute :href, :string
7
+ attribute :http_method, :string
8
+ attribute :rel, :string
9
+ attribute :title, :string
10
+ attribute :description, :string
11
+ attribute :schema, Schema
12
+ attribute :target_schema, Schema
13
+
14
+ json do
15
+ map "href", to: :href
16
+ map "method", to: :http_method
17
+ map "rel", to: :rel
18
+ map "title", to: :title
19
+ map "description", to: :description
20
+ map "schema", to: :schema
21
+ map "targetSchema", to: :target_schema
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Jsonschema
5
+ class PropertyEntry < Base
6
+ attribute :name, :string
7
+ attribute :schema, Schema
8
+
9
+ json do
10
+ map "name", to: :name
11
+ map "schema", to: :schema
12
+ end
13
+ end
14
+ end
15
+ end