lutaml-xsd 1.0.9 → 1.1.1
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.
- checksums.yaml +4 -4
- data/.rubocop_todo.yml +26 -68
- data/lib/lutaml/xsd/spa/schema_serializer.rb +302 -118
- data/lib/lutaml/xsd/version.rb +1 -1
- data/lutaml-xsd.gemspec +1 -1
- metadata +12 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 29e9ba5ed3edf116b76cdf22ed853a37399d00ffda6b11f78a0cccdb5502b4e4
|
|
4
|
+
data.tar.gz: 6213f84485cad0ffed1eef19d60ae29fea7d5e274522ba8dfcc996048694e677
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 07a2c5d774d76218e043372749950daa031c3b1ff3177a4eb65c5a05de6e6747479bbbd4acd7a5c7a808553bb75e62808d66bb68157324b4341d25045e11aa0b
|
|
7
|
+
data.tar.gz: cba0111db7b0b7bf12fb9e0c55edbdfcbc4e3f4f11a653957ad663e6d8c067a1980bcbb7a54898f50b6e408869ca56d0179a822a7fcae8ba201111c68fceeb15
|
data/.rubocop_todo.yml
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# This configuration was generated by
|
|
2
2
|
# `rubocop --auto-gen-config`
|
|
3
|
-
# on 2026-04-
|
|
3
|
+
# on 2026-04-27 11:46:52 UTC using RuboCop version 1.86.1.
|
|
4
4
|
# The point is for the user to remove these configuration records
|
|
5
5
|
# one by one as the offenses are removed from the code base.
|
|
6
6
|
# Note that changes in the inspected code, or installation of new
|
|
@@ -11,56 +11,21 @@ Gemspec/RequiredRubyVersion:
|
|
|
11
11
|
Exclude:
|
|
12
12
|
- 'lutaml-xsd.gemspec'
|
|
13
13
|
|
|
14
|
-
# Offense count: 2
|
|
15
|
-
# This cop supports safe autocorrection (--autocorrect).
|
|
16
|
-
# Configuration parameters: EnforcedStyle, IndentationWidth.
|
|
17
|
-
# SupportedStyles: with_first_element, with_fixed_indentation
|
|
18
|
-
Layout/ArrayAlignment:
|
|
19
|
-
Exclude:
|
|
20
|
-
- 'lib/lutaml/xsd/spa/schema_serializer.rb'
|
|
21
|
-
- 'lib/lutaml/xsd/spa/utils/extract_enumeration.rb'
|
|
22
|
-
|
|
23
|
-
# Offense count: 4
|
|
24
|
-
# This cop supports safe autocorrection (--autocorrect).
|
|
25
|
-
# Configuration parameters: IndentationWidth.
|
|
26
|
-
Layout/AssignmentIndentation:
|
|
27
|
-
Exclude:
|
|
28
|
-
- 'lib/lutaml/xsd/commands/package_command.rb'
|
|
29
|
-
- 'lib/lutaml/xsd/spa/schema_serializer.rb'
|
|
30
|
-
|
|
31
|
-
# Offense count: 7
|
|
32
|
-
# This cop supports safe autocorrection (--autocorrect).
|
|
33
|
-
Layout/ClosingParenthesisIndentation:
|
|
34
|
-
Exclude:
|
|
35
|
-
- 'lib/lutaml/xsd/spa/schema_serializer.rb'
|
|
36
|
-
|
|
37
14
|
# Offense count: 1
|
|
38
15
|
# This cop supports safe autocorrection (--autocorrect).
|
|
39
16
|
# Configuration parameters: EnforcedStyleAlignWith.
|
|
40
|
-
# SupportedStylesAlignWith:
|
|
41
|
-
Layout/
|
|
17
|
+
# SupportedStylesAlignWith: either, start_of_block, start_of_line
|
|
18
|
+
Layout/BlockAlignment:
|
|
42
19
|
Exclude:
|
|
43
20
|
- 'lib/lutaml/xsd/spa/schema_serializer.rb'
|
|
44
21
|
|
|
45
22
|
# Offense count: 1
|
|
46
23
|
# This cop supports safe autocorrection (--autocorrect).
|
|
47
|
-
|
|
48
|
-
# SupportedStyles: consistent, consistent_relative_to_receiver, special_for_inner_method_call, special_for_inner_method_call_in_parentheses
|
|
49
|
-
Layout/FirstArgumentIndentation:
|
|
50
|
-
Exclude:
|
|
51
|
-
- 'lib/lutaml/xsd/spa/schema_serializer.rb'
|
|
52
|
-
|
|
53
|
-
# Offense count: 3
|
|
54
|
-
# This cop supports safe autocorrection (--autocorrect).
|
|
55
|
-
# Configuration parameters: AllowMultipleStyles, EnforcedHashRocketStyle, EnforcedColonStyle, EnforcedLastArgumentHashStyle.
|
|
56
|
-
# SupportedHashRocketStyles: key, separator, table
|
|
57
|
-
# SupportedColonStyles: key, separator, table
|
|
58
|
-
# SupportedLastArgumentHashStyles: always_inspect, always_ignore, ignore_implicit, ignore_explicit
|
|
59
|
-
Layout/HashAlignment:
|
|
24
|
+
Layout/BlockEndNewline:
|
|
60
25
|
Exclude:
|
|
61
26
|
- 'lib/lutaml/xsd/spa/schema_serializer.rb'
|
|
62
27
|
|
|
63
|
-
# Offense count:
|
|
28
|
+
# Offense count: 2
|
|
64
29
|
# This cop supports safe autocorrection (--autocorrect).
|
|
65
30
|
# Configuration parameters: Width, EnforcedStyleAlignWith, AllowedPatterns.
|
|
66
31
|
# SupportedStylesAlignWith: start_of_line, relative_to_receiver
|
|
@@ -68,30 +33,20 @@ Layout/IndentationWidth:
|
|
|
68
33
|
Exclude:
|
|
69
34
|
- 'lib/lutaml/xsd/spa/schema_serializer.rb'
|
|
70
35
|
|
|
71
|
-
# Offense count:
|
|
36
|
+
# Offense count: 722
|
|
72
37
|
# This cop supports safe autocorrection (--autocorrect).
|
|
73
38
|
# Configuration parameters: Max, AllowHeredoc, AllowURI, AllowQualifiedName, URISchemes, AllowRBSInlineAnnotation, AllowCopDirectives, AllowedPatterns, SplitStrings.
|
|
74
39
|
# URISchemes: http, https
|
|
75
40
|
Layout/LineLength:
|
|
76
41
|
Enabled: false
|
|
77
42
|
|
|
78
|
-
# Offense count:
|
|
79
|
-
# This cop supports safe autocorrection (--autocorrect).
|
|
80
|
-
# Configuration parameters: EnforcedStyle.
|
|
81
|
-
# SupportedStyles: symmetrical, new_line, same_line
|
|
82
|
-
Layout/MultilineMethodCallBraceLayout:
|
|
83
|
-
Exclude:
|
|
84
|
-
- 'lib/lutaml/xsd/spa/schema_serializer.rb'
|
|
85
|
-
|
|
86
|
-
# Offense count: 11
|
|
43
|
+
# Offense count: 1
|
|
87
44
|
# This cop supports safe autocorrection (--autocorrect).
|
|
88
|
-
# Configuration parameters:
|
|
89
|
-
|
|
45
|
+
# Configuration parameters: EnforcedStyle, IndentationWidth.
|
|
46
|
+
# SupportedStyles: aligned, indented
|
|
47
|
+
Layout/MultilineOperationIndentation:
|
|
90
48
|
Exclude:
|
|
91
|
-
- 'lib/lutaml/xsd/commands/package_command.rb'
|
|
92
49
|
- 'lib/lutaml/xsd/spa/schema_serializer.rb'
|
|
93
|
-
- 'lib/lutaml/xsd/spa/utils/extract_enumeration.rb'
|
|
94
|
-
- 'lib/lutaml/xsd/spa/xml_instance_generator.rb'
|
|
95
50
|
|
|
96
51
|
# Offense count: 11
|
|
97
52
|
# Configuration parameters: IgnoreLiteralBranches, IgnoreConstantBranches, IgnoreDuplicateElseBranch.
|
|
@@ -126,7 +81,7 @@ Lint/UselessRescue:
|
|
|
126
81
|
Exclude:
|
|
127
82
|
- 'lib/lutaml/xsd/validation/rule_engine.rb'
|
|
128
83
|
|
|
129
|
-
# Offense count:
|
|
84
|
+
# Offense count: 269
|
|
130
85
|
# Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes, Max.
|
|
131
86
|
Metrics/AbcSize:
|
|
132
87
|
Enabled: false
|
|
@@ -142,12 +97,12 @@ Metrics/BlockLength:
|
|
|
142
97
|
Metrics/BlockNesting:
|
|
143
98
|
Max: 5
|
|
144
99
|
|
|
145
|
-
# Offense count:
|
|
100
|
+
# Offense count: 201
|
|
146
101
|
# Configuration parameters: AllowedMethods, AllowedPatterns, Max.
|
|
147
102
|
Metrics/CyclomaticComplexity:
|
|
148
103
|
Enabled: false
|
|
149
104
|
|
|
150
|
-
# Offense count:
|
|
105
|
+
# Offense count: 373
|
|
151
106
|
# Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns.
|
|
152
107
|
Metrics/MethodLength:
|
|
153
108
|
Max: 90
|
|
@@ -157,12 +112,12 @@ Metrics/MethodLength:
|
|
|
157
112
|
Metrics/ParameterLists:
|
|
158
113
|
Max: 9
|
|
159
114
|
|
|
160
|
-
# Offense count:
|
|
115
|
+
# Offense count: 151
|
|
161
116
|
# Configuration parameters: AllowedMethods, AllowedPatterns, Max.
|
|
162
117
|
Metrics/PerceivedComplexity:
|
|
163
118
|
Enabled: false
|
|
164
119
|
|
|
165
|
-
# Offense count:
|
|
120
|
+
# Offense count: 25
|
|
166
121
|
# Configuration parameters: MinNameLength, AllowNamesEndingInNumbers, AllowedNames, ForbiddenNames.
|
|
167
122
|
# AllowedNames: as, at, by, cc, db, id, if, in, io, ip, of, on, os, pp, to
|
|
168
123
|
Naming/MethodParameterName:
|
|
@@ -222,6 +177,17 @@ Security/MarshalLoad:
|
|
|
222
177
|
Exclude:
|
|
223
178
|
- 'lib/lutaml/xsd/package_builder.rb'
|
|
224
179
|
|
|
180
|
+
# Offense count: 1
|
|
181
|
+
# This cop supports safe autocorrection (--autocorrect).
|
|
182
|
+
# Configuration parameters: EnforcedStyle, ProceduralMethods, FunctionalMethods, AllowedMethods, AllowedPatterns, AllowBracesOnProceduralOneLiners, BracesRequiredMethods.
|
|
183
|
+
# SupportedStyles: line_count_based, semantic, braces_for_chaining, always_braces
|
|
184
|
+
# ProceduralMethods: benchmark, bm, bmbm, create, each_with_object, measure, new, realtime, tap, with_object
|
|
185
|
+
# FunctionalMethods: let, let!, subject, watch
|
|
186
|
+
# AllowedMethods: lambda, proc, it
|
|
187
|
+
Style/BlockDelimiters:
|
|
188
|
+
Exclude:
|
|
189
|
+
- 'lib/lutaml/xsd/spa/schema_serializer.rb'
|
|
190
|
+
|
|
225
191
|
# Offense count: 6
|
|
226
192
|
Style/DocumentDynamicEvalDefinition:
|
|
227
193
|
Exclude:
|
|
@@ -248,14 +214,6 @@ Style/IdenticalConditionalBranches:
|
|
|
248
214
|
Exclude:
|
|
249
215
|
- 'lib/lutaml/xsd/package_tree_formatter.rb'
|
|
250
216
|
|
|
251
|
-
# Offense count: 3
|
|
252
|
-
# This cop supports safe autocorrection (--autocorrect).
|
|
253
|
-
Style/MultilineIfModifier:
|
|
254
|
-
Exclude:
|
|
255
|
-
- 'lib/lutaml/xsd/commands/package_command.rb'
|
|
256
|
-
- 'lib/lutaml/xsd/spa/schema_serializer.rb'
|
|
257
|
-
- 'lib/lutaml/xsd/spa/utils/extract_enumeration.rb'
|
|
258
|
-
|
|
259
217
|
# Offense count: 1
|
|
260
218
|
Style/OpenStructUse:
|
|
261
219
|
Exclude:
|
|
@@ -4,6 +4,9 @@ require "json"
|
|
|
4
4
|
require "moxml"
|
|
5
5
|
require "tmpdir"
|
|
6
6
|
require "fileutils"
|
|
7
|
+
require "tempfile"
|
|
8
|
+
require "nokogiri"
|
|
9
|
+
require "xsdvi"
|
|
7
10
|
require_relative "xml_instance_generator"
|
|
8
11
|
require_relative "utils/extract_enumeration"
|
|
9
12
|
|
|
@@ -32,6 +35,12 @@ module Lutaml
|
|
|
32
35
|
class SchemaSerializer
|
|
33
36
|
include ::Lutaml::Xsd::Spa::Utils::ExtractEnumeration
|
|
34
37
|
|
|
38
|
+
# Fields to merge when combining schemas with the same targetNamespace
|
|
39
|
+
MERGEABLE_CONTENT_FIELDS = %i[
|
|
40
|
+
elements complex_types simple_types
|
|
41
|
+
attributes groups attribute_groups
|
|
42
|
+
].freeze
|
|
43
|
+
|
|
35
44
|
attr_reader :repository, :config, :package
|
|
36
45
|
|
|
37
46
|
# Initialize schema serializer
|
|
@@ -214,12 +223,104 @@ module Lutaml
|
|
|
214
223
|
serialize_schema(schema, index, file_path)
|
|
215
224
|
end.compact
|
|
216
225
|
|
|
226
|
+
# Merge schemas that share the same target namespace (from <include>)
|
|
227
|
+
# XSD <include> merges included schemas into the same namespace.
|
|
228
|
+
# Group by namespace, keep entry point as primary, merge content from others.
|
|
229
|
+
schemas_data = merge_included_schemas(schemas_data)
|
|
230
|
+
|
|
217
231
|
# Post-process: add used_by reverse references
|
|
218
232
|
attach_used_by_references(schemas_data)
|
|
219
233
|
|
|
220
234
|
schemas_data
|
|
221
235
|
end
|
|
222
236
|
|
|
237
|
+
# Merge schemas sharing the same targetNamespace (from <include> directives)
|
|
238
|
+
#
|
|
239
|
+
# In XSD, <include> means the included schema targets the same namespace.
|
|
240
|
+
# These should appear as a single merged schema in the SPA, not as
|
|
241
|
+
# separate empty + populated entries.
|
|
242
|
+
#
|
|
243
|
+
# Schemas with nil namespace (chameleon schemas) are NOT merged since
|
|
244
|
+
# they adopt the namespace of their including schema at parse time.
|
|
245
|
+
#
|
|
246
|
+
# @param schemas_data [Array<Hash>] Serialized schema data
|
|
247
|
+
# @return [Array<Hash>] Merged schema data
|
|
248
|
+
def merge_included_schemas(schemas_data)
|
|
249
|
+
return schemas_data if schemas_data.length <= 1
|
|
250
|
+
|
|
251
|
+
ns_groups = {}
|
|
252
|
+
schemas_data.each do |schema|
|
|
253
|
+
ns = schema[:namespace]
|
|
254
|
+
# Skip nil-namespace schemas — chameleon schemas should not be merged
|
|
255
|
+
next unless ns
|
|
256
|
+
|
|
257
|
+
ns_groups[ns] ||= []
|
|
258
|
+
ns_groups[ns] << schema
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
# Collect schemas that were NOT grouped (nil namespace)
|
|
262
|
+
merged_schemas = schemas_data.reject { |s| s[:namespace] }
|
|
263
|
+
|
|
264
|
+
ns_groups.each_value do |group|
|
|
265
|
+
if group.length == 1
|
|
266
|
+
merged_schemas << group.first
|
|
267
|
+
next
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
primary = group.find { |s| s[:is_entrypoint] }
|
|
271
|
+
primary ||= group.max_by { |s| content_weight(s) }
|
|
272
|
+
secondaries = group.reject { |s| s[:id] == primary[:id] }
|
|
273
|
+
|
|
274
|
+
merge_content_into!(primary, secondaries)
|
|
275
|
+
merged_schemas << primary
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
merged_schemas
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
# Measure content richness of a serialized schema for primary selection
|
|
282
|
+
#
|
|
283
|
+
# @param schema [Hash] Serialized schema data
|
|
284
|
+
# @return [Integer] Total item count across content fields
|
|
285
|
+
def content_weight(schema)
|
|
286
|
+
MERGEABLE_CONTENT_FIELDS.sum { |f| (schema[f] || []).length }
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
# Merge content arrays from secondary schemas into the primary schema
|
|
290
|
+
#
|
|
291
|
+
# Uses Set for O(1) deduplication by hash identity.
|
|
292
|
+
#
|
|
293
|
+
# @param primary [Hash] Primary schema to merge into (mutated)
|
|
294
|
+
# @param secondaries [Array<Hash>] Secondary schemas to absorb
|
|
295
|
+
# @return [void]
|
|
296
|
+
def merge_content_into!(primary, secondaries)
|
|
297
|
+
MERGEABLE_CONTENT_FIELDS.each do |field|
|
|
298
|
+
primary[field] ||= []
|
|
299
|
+
seen = primary[field].to_set
|
|
300
|
+
secondaries.each do |sec|
|
|
301
|
+
(sec[field] || []).each do |item|
|
|
302
|
+
primary[field] << item unless seen.include?(item)
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
# Merge includes and imports (deduplicated by hash equality)
|
|
308
|
+
%i[includes imports].each do |field|
|
|
309
|
+
primary[field] ||= []
|
|
310
|
+
seen = primary[field].to_set
|
|
311
|
+
secondaries.each do |sec|
|
|
312
|
+
(sec[field] || []).each do |item|
|
|
313
|
+
primary[field] << item unless seen.include?(item)
|
|
314
|
+
end
|
|
315
|
+
end
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
# Collect all file paths
|
|
319
|
+
all_paths = [primary[:file_path]]
|
|
320
|
+
secondaries.each { |s| all_paths << s[:file_path] if s[:file_path] }
|
|
321
|
+
primary[:file_paths] = all_paths.compact
|
|
322
|
+
end
|
|
323
|
+
|
|
223
324
|
# Serialize single schema (template method hook)
|
|
224
325
|
#
|
|
225
326
|
# Subclasses should override to customize schema serialization
|
|
@@ -355,23 +456,48 @@ module Lutaml
|
|
|
355
456
|
element_data
|
|
356
457
|
end
|
|
357
458
|
|
|
358
|
-
def gen_element_diagram(name, file_path)
|
|
459
|
+
def gen_element_diagram(name, file_path, component_type = :element)
|
|
359
460
|
if !file_path || !File.exist?(file_path)
|
|
360
461
|
warn "xsdvi: XSD file '#{file_path}' not found" if config[:verbose]
|
|
361
462
|
return nil
|
|
362
463
|
end
|
|
363
464
|
|
|
364
|
-
|
|
465
|
+
actual_file_path = file_path
|
|
466
|
+
wrapper_name = name
|
|
365
467
|
|
|
366
|
-
|
|
367
|
-
|
|
468
|
+
if component_type == :type
|
|
469
|
+
# For types, create a temporary XSD with a synthetic wrapper element
|
|
470
|
+
# so xsdvi can treat the type as a root element
|
|
471
|
+
actual_file_path = create_type_wrapper_xsd(name, file_path)
|
|
472
|
+
wrapper_name = "_diagram_root_#{name}"
|
|
473
|
+
end
|
|
368
474
|
|
|
475
|
+
output_folder = Dir.mktmpdir("xsdvi-")
|
|
369
476
|
svg_file = File.join(output_folder, "#{name}.svg")
|
|
370
|
-
|
|
477
|
+
|
|
478
|
+
# Use xsdvi Ruby API directly instead of CLI
|
|
479
|
+
builder = Xsdvi::Tree::Builder.new
|
|
480
|
+
xsd_handler = Xsdvi::XsdHandler.new(builder)
|
|
481
|
+
writer_helper = Xsdvi::Utils::Writer.new
|
|
482
|
+
svg_generator = Xsdvi::SVG::Generator.new(writer_helper)
|
|
483
|
+
|
|
484
|
+
svg_generator.hide_menu_buttons = true
|
|
485
|
+
svg_generator.embody_style = true
|
|
486
|
+
|
|
487
|
+
xsd_handler.root_node_name = wrapper_name
|
|
488
|
+
xsd_handler.one_node_only = true
|
|
489
|
+
xsd_handler.process_file(actual_file_path)
|
|
490
|
+
|
|
491
|
+
unless builder.root
|
|
371
492
|
warn "xsdvi: SVG not generated for '#{name}'" if config[:verbose]
|
|
372
493
|
return nil
|
|
373
494
|
end
|
|
374
495
|
|
|
496
|
+
writer_helper.new_writer(svg_file)
|
|
497
|
+
svg_generator.draw(builder.root)
|
|
498
|
+
|
|
499
|
+
return nil unless File.exist?(svg_file)
|
|
500
|
+
|
|
375
501
|
# read generated SVG content
|
|
376
502
|
svg_content = File.read(svg_file)
|
|
377
503
|
|
|
@@ -384,8 +510,84 @@ module Lutaml
|
|
|
384
510
|
.gsub(/<a[^>]*>(.*?)<\/a>/im, '\1') # Remove links but keep content
|
|
385
511
|
|
|
386
512
|
svg_content
|
|
513
|
+
rescue StandardError => e
|
|
514
|
+
warn "xsdvi: Failed to generate diagram for '#{name}': #{e.message}" if config[:verbose]
|
|
515
|
+
nil
|
|
387
516
|
ensure
|
|
388
517
|
FileUtils.rm_rf(output_folder) if output_folder
|
|
518
|
+
if component_type == :type && actual_file_path && actual_file_path != file_path
|
|
519
|
+
FileUtils.rm_f(actual_file_path)
|
|
520
|
+
end
|
|
521
|
+
end
|
|
522
|
+
|
|
523
|
+
# Create a temporary XSD file that wraps a complexType/simpleType
|
|
524
|
+
# in a synthetic root element, so xsdvi can generate a diagram for it.
|
|
525
|
+
#
|
|
526
|
+
# @param type_name [String] The type name
|
|
527
|
+
# @param original_file_path [String] Path to the original XSD file
|
|
528
|
+
# @return [String] Path to the temporary XSD file
|
|
529
|
+
def create_type_wrapper_xsd(type_name, original_file_path)
|
|
530
|
+
doc = Nokogiri::XML(File.read(original_file_path))
|
|
531
|
+
ns = { "xs" => "http://www.w3.org/2001/XMLSchema" }
|
|
532
|
+
schema = doc.at_xpath("//xs:schema", ns)
|
|
533
|
+
|
|
534
|
+
unless schema
|
|
535
|
+
warn "xsdvi: No xs:schema found in '#{original_file_path}'" if config[:verbose]
|
|
536
|
+
return original_file_path
|
|
537
|
+
end
|
|
538
|
+
|
|
539
|
+
wrapper_name = "_diagram_root_#{type_name}"
|
|
540
|
+
|
|
541
|
+
# Check if this type might need a namespace prefix
|
|
542
|
+
# Look for how the type is referenced in the original schema
|
|
543
|
+
type_ref = resolve_type_prefix(type_name, doc, ns)
|
|
544
|
+
|
|
545
|
+
# Create element with proper XSD namespace from the schema
|
|
546
|
+
xsd_ns = "http://www.w3.org/2001/XMLSchema"
|
|
547
|
+
xsd_ns_decl = schema.namespace_definitions.find do |nd|
|
|
548
|
+
nd.href == xsd_ns
|
|
549
|
+
end
|
|
550
|
+
|
|
551
|
+
element_node = doc.create_element("element")
|
|
552
|
+
element_node.namespace = xsd_ns_decl
|
|
553
|
+
element_node["name"] = wrapper_name
|
|
554
|
+
element_node["type"] = type_ref
|
|
555
|
+
|
|
556
|
+
schema.add_child(element_node)
|
|
557
|
+
|
|
558
|
+
tmp = Tempfile.new(["xsdvi-wrapper-", ".xsd"])
|
|
559
|
+
tmp.write(doc.to_xml)
|
|
560
|
+
tmp.close
|
|
561
|
+
tmp.path
|
|
562
|
+
end
|
|
563
|
+
|
|
564
|
+
# Determine the correct type reference prefix for a type in the schema.
|
|
565
|
+
# If the type is defined in the targetNamespace, use the same prefix
|
|
566
|
+
# that the schema uses internally for its targetNamespace.
|
|
567
|
+
#
|
|
568
|
+
# @param type_name [String] The type name
|
|
569
|
+
# @param doc [Nokogiri::Document] The parsed XSD document
|
|
570
|
+
# @param ns [Hash] Namespace mapping
|
|
571
|
+
# @return [String] The type reference string (possibly prefixed)
|
|
572
|
+
def resolve_type_prefix(type_name, doc, ns)
|
|
573
|
+
schema = doc.at_xpath("//xs:schema", ns)
|
|
574
|
+
target_ns = schema["targetNamespace"]
|
|
575
|
+
|
|
576
|
+
# Check if the type is defined in this schema's targetNamespace
|
|
577
|
+
type_in_schema = doc.xpath("//xs:complexType[@name='#{type_name}']",
|
|
578
|
+
ns).any? ||
|
|
579
|
+
doc.xpath("//xs:simpleType[@name='#{type_name}']",
|
|
580
|
+
ns).any?
|
|
581
|
+
|
|
582
|
+
return type_name unless type_in_schema && target_ns
|
|
583
|
+
|
|
584
|
+
# Find what prefix maps to the targetNamespace
|
|
585
|
+
ns_decls = schema.namespace_definitions
|
|
586
|
+
tns_prefix = ns_decls.find do |nd|
|
|
587
|
+
nd.href == target_ns && nd.prefix
|
|
588
|
+
end
|
|
589
|
+
|
|
590
|
+
tns_prefix ? "#{tns_prefix.prefix}:#{type_name}" : type_name
|
|
389
591
|
end
|
|
390
592
|
|
|
391
593
|
# Serialize complex types from schema
|
|
@@ -642,24 +844,29 @@ schema_source = nil)
|
|
|
642
844
|
end.sort_by { |ag| ag[:name] || "" }
|
|
643
845
|
end
|
|
644
846
|
|
|
645
|
-
# Extract source
|
|
646
|
-
#
|
|
847
|
+
# Extract source XML for a schema component identified by type, key, value
|
|
848
|
+
#
|
|
849
|
+
# @param type [String] XSD element type name (e.g., "attributeGroup")
|
|
850
|
+
# @param key [String] Attribute name to match on (e.g., "name")
|
|
851
|
+
# @param value [String] Attribute value to match
|
|
852
|
+
# @param prefix [String, nil] Optional namespace prefix
|
|
853
|
+
# @param source [String, nil] Raw XSD source XML
|
|
854
|
+
# @return [String, nil] Extracted source XML or nil
|
|
647
855
|
def extract_source_by_type_key_value(type, key, value, prefix = nil,
|
|
648
856
|
source = nil)
|
|
649
857
|
return nil unless source && value
|
|
650
858
|
|
|
651
|
-
# parse the schema source and find the attribute group by name
|
|
652
859
|
begin
|
|
653
860
|
doc = Moxml::Context.new.parse(source)
|
|
861
|
+
escaped_value = value.gsub("'", "''")
|
|
654
862
|
xpath = if prefix
|
|
655
|
-
"//#{prefix}:#{type}[@#{key}='#{
|
|
863
|
+
"//#{prefix}:#{type}[@#{key}='#{escaped_value}']"
|
|
656
864
|
else
|
|
657
|
-
"//#{type}[@#{key}='#{
|
|
865
|
+
"//#{type}[@#{key}='#{escaped_value}']"
|
|
658
866
|
end
|
|
659
|
-
|
|
660
|
-
|
|
867
|
+
node = doc.at_xpath(xpath)
|
|
868
|
+
node&.to_xml(indent: 2)
|
|
661
869
|
rescue StandardError
|
|
662
|
-
# If parsing fails, return nil
|
|
663
870
|
nil
|
|
664
871
|
end
|
|
665
872
|
end
|
|
@@ -785,45 +992,52 @@ source = nil)
|
|
|
785
992
|
end
|
|
786
993
|
end
|
|
787
994
|
|
|
995
|
+
# Collect attribute group references from a model (handles extension nesting)
|
|
996
|
+
#
|
|
997
|
+
# Used for both direct attribute groups and those inside content model extensions.
|
|
998
|
+
#
|
|
999
|
+
# @param model [Object] Any object that may have attribute_group or extension
|
|
1000
|
+
# @return [Array<Object>] Collected attribute group reference objects
|
|
1001
|
+
def collect_attribute_group_refs(model)
|
|
1002
|
+
refs = []
|
|
1003
|
+
|
|
1004
|
+
if model.respond_to?(:attribute_group) && model.attribute_group
|
|
1005
|
+
groups = model.attribute_group.is_a?(Array) ? model.attribute_group : [model.attribute_group]
|
|
1006
|
+
refs.concat(groups)
|
|
1007
|
+
end
|
|
1008
|
+
|
|
1009
|
+
if model.respond_to?(:extension) && model.extension
|
|
1010
|
+
refs.concat(collect_attribute_group_refs(model.extension))
|
|
1011
|
+
end
|
|
1012
|
+
|
|
1013
|
+
refs
|
|
1014
|
+
end
|
|
1015
|
+
|
|
788
1016
|
# Serialize attribute group references from a complex type
|
|
789
1017
|
#
|
|
1018
|
+
# Collects attribute group refs from three possible locations:
|
|
1019
|
+
# 1. Direct attribute groups on the type
|
|
1020
|
+
# 2. Inside simpleContent.extension
|
|
1021
|
+
# 3. Inside complexContent.extension
|
|
1022
|
+
#
|
|
790
1023
|
# @param type [ComplexType] Complex type
|
|
791
1024
|
# @return [Array<Hash>] Serialized attribute group references with attributes
|
|
792
1025
|
def serialize_type_attr_groups(type)
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
# 2. Inside simpleContent.extension
|
|
796
|
-
# 3. Inside complexContent.extension
|
|
1026
|
+
return [] unless type
|
|
1027
|
+
|
|
797
1028
|
all_ag_refs = []
|
|
798
1029
|
|
|
799
|
-
# 1. Direct attribute groups
|
|
800
1030
|
if type.respond_to?(:attribute_group) && type.attribute_group
|
|
801
1031
|
direct_groups = type.attribute_group.is_a?(Array) ? type.attribute_group : [type.attribute_group]
|
|
802
1032
|
all_ag_refs.concat(direct_groups)
|
|
803
1033
|
end
|
|
804
1034
|
|
|
805
|
-
# 2. Attribute groups inside simpleContent.extension
|
|
806
1035
|
if type.respond_to?(:simple_content) && type.simple_content
|
|
807
|
-
|
|
808
|
-
if sc.respond_to?(:extension) && sc.extension
|
|
809
|
-
extension = sc.extension
|
|
810
|
-
if extension.respond_to?(:attribute_group) && extension.attribute_group
|
|
811
|
-
ext_groups = extension.attribute_group.is_a?(Array) ? extension.attribute_group : [extension.attribute_group]
|
|
812
|
-
all_ag_refs.concat(ext_groups)
|
|
813
|
-
end
|
|
814
|
-
end
|
|
1036
|
+
all_ag_refs.concat(collect_attribute_group_refs(type.simple_content))
|
|
815
1037
|
end
|
|
816
1038
|
|
|
817
|
-
# 3. Attribute groups inside complexContent.extension
|
|
818
1039
|
if type.respond_to?(:complex_content) && type.complex_content
|
|
819
|
-
|
|
820
|
-
if cc.respond_to?(:extension) && cc.extension
|
|
821
|
-
extension = cc.extension
|
|
822
|
-
if extension.respond_to?(:attribute_group) && extension.attribute_group
|
|
823
|
-
ext_groups = extension.attribute_group.is_a?(Array) ? extension.attribute_group : [extension.attribute_group]
|
|
824
|
-
all_ag_refs.concat(ext_groups)
|
|
825
|
-
end
|
|
826
|
-
end
|
|
1040
|
+
all_ag_refs.concat(collect_attribute_group_refs(type.complex_content))
|
|
827
1041
|
end
|
|
828
1042
|
|
|
829
1043
|
return [] if all_ag_refs.empty?
|
|
@@ -832,12 +1046,8 @@ source = nil)
|
|
|
832
1046
|
ag_name = ag.respond_to?(:ref) ? ag.ref : ag.name
|
|
833
1047
|
next unless ag_name
|
|
834
1048
|
|
|
835
|
-
# Look up the actual attribute group definition to get its attributes
|
|
836
1049
|
attrs = lookup_attribute_group_attributes(ag_name)
|
|
837
|
-
{
|
|
838
|
-
ref: ag_name,
|
|
839
|
-
attributes: attrs,
|
|
840
|
-
}
|
|
1050
|
+
{ ref: ag_name, attributes: attrs }
|
|
841
1051
|
end
|
|
842
1052
|
end
|
|
843
1053
|
|
|
@@ -1076,74 +1286,43 @@ source = nil)
|
|
|
1076
1286
|
}
|
|
1077
1287
|
end
|
|
1078
1288
|
|
|
1289
|
+
# Facet serializers: maps facet name to how to extract and format it
|
|
1290
|
+
#
|
|
1291
|
+
# Each entry: [method_name, facet_type_label]
|
|
1292
|
+
# method_name — what restriction.respond_to?(:method_name) && restriction.method_name to call
|
|
1293
|
+
# facet_type — the type string emitted in serialized facet
|
|
1294
|
+
FACET_METHODS = [
|
|
1295
|
+
[:enumerations, "enumeration"],
|
|
1296
|
+
[:pattern, "pattern"],
|
|
1297
|
+
[:min_length, "min_length"],
|
|
1298
|
+
[:max_length, "max_length"],
|
|
1299
|
+
[:length, "length"],
|
|
1300
|
+
[:min_inclusive, "min_inclusive"],
|
|
1301
|
+
[:max_inclusive, "max_inclusive"],
|
|
1302
|
+
[:min_exclusive, "min_exclusive"],
|
|
1303
|
+
[:max_exclusive, "max_exclusive"],
|
|
1304
|
+
[:total_digits, "total_digits"],
|
|
1305
|
+
[:fraction_digits, "fraction_digits"],
|
|
1306
|
+
[:white_space, "white_space"],
|
|
1307
|
+
].freeze
|
|
1308
|
+
|
|
1079
1309
|
# Serialize facets
|
|
1080
1310
|
#
|
|
1081
1311
|
# @param restriction [Restriction] Restriction object
|
|
1082
1312
|
# @return [Array<Hash>] Serialized facets
|
|
1083
1313
|
def serialize_facets(restriction)
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
if restriction.respond_to?(:enumerations) && restriction.enumerations
|
|
1087
|
-
facets << { type: "enumeration",
|
|
1088
|
-
values: restriction.enumerations }
|
|
1089
|
-
end
|
|
1090
|
-
|
|
1091
|
-
if restriction.respond_to?(:pattern) && restriction.pattern
|
|
1092
|
-
facets << { type: "pattern",
|
|
1093
|
-
value: restriction.pattern }
|
|
1094
|
-
end
|
|
1314
|
+
return [] unless restriction
|
|
1095
1315
|
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
end
|
|
1316
|
+
FACET_METHODS.filter_map do |method_name, facet_type|
|
|
1317
|
+
value = restriction.respond_to?(method_name) && restriction.send(method_name)
|
|
1318
|
+
next unless value
|
|
1100
1319
|
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
if restriction.respond_to?(:length) && restriction.length
|
|
1107
|
-
facets << { type: "length",
|
|
1108
|
-
value: restriction.length }
|
|
1109
|
-
end
|
|
1110
|
-
|
|
1111
|
-
if restriction.respond_to?(:min_inclusive) && restriction.min_inclusive
|
|
1112
|
-
facets << { type: "min_inclusive",
|
|
1113
|
-
value: restriction.min_inclusive }
|
|
1114
|
-
end
|
|
1115
|
-
|
|
1116
|
-
if restriction.respond_to?(:max_inclusive) && restriction.max_inclusive
|
|
1117
|
-
facets << { type: "max_inclusive",
|
|
1118
|
-
value: restriction.max_inclusive }
|
|
1119
|
-
end
|
|
1120
|
-
|
|
1121
|
-
if restriction.respond_to?(:min_exclusive) && restriction.min_exclusive
|
|
1122
|
-
facets << { type: "min_exclusive",
|
|
1123
|
-
value: restriction.min_exclusive }
|
|
1124
|
-
end
|
|
1125
|
-
|
|
1126
|
-
if restriction.respond_to?(:max_exclusive) && restriction.max_exclusive
|
|
1127
|
-
facets << { type: "max_exclusive",
|
|
1128
|
-
value: restriction.max_exclusive }
|
|
1129
|
-
end
|
|
1130
|
-
|
|
1131
|
-
if restriction.respond_to?(:total_digits) && restriction.total_digits
|
|
1132
|
-
facets << { type: "total_digits",
|
|
1133
|
-
value: restriction.total_digits }
|
|
1134
|
-
end
|
|
1135
|
-
|
|
1136
|
-
if restriction.respond_to?(:fraction_digits) && restriction.fraction_digits
|
|
1137
|
-
facets << { type: "fraction_digits",
|
|
1138
|
-
value: restriction.fraction_digits }
|
|
1139
|
-
end
|
|
1140
|
-
|
|
1141
|
-
if restriction.respond_to?(:white_space) && restriction.white_space
|
|
1142
|
-
facets << { type: "white_space",
|
|
1143
|
-
value: restriction.white_space }
|
|
1320
|
+
if method_name == :enumerations
|
|
1321
|
+
{ type: facet_type, values: value }
|
|
1322
|
+
else
|
|
1323
|
+
{ type: facet_type, value: value }
|
|
1324
|
+
end
|
|
1144
1325
|
end
|
|
1145
|
-
|
|
1146
|
-
facets
|
|
1147
1326
|
end
|
|
1148
1327
|
|
|
1149
1328
|
# Extract documentation from object
|
|
@@ -1175,29 +1354,34 @@ source = nil)
|
|
|
1175
1354
|
"empty"
|
|
1176
1355
|
end
|
|
1177
1356
|
|
|
1357
|
+
# Extract base type from a content model's extension or restriction
|
|
1358
|
+
#
|
|
1359
|
+
# @param content_model [Object] simpleContent or complexContent
|
|
1360
|
+
# @return [String, nil] Base type name
|
|
1361
|
+
def base_from_content_model(content_model)
|
|
1362
|
+
if content_model.respond_to?(:extension) && content_model.extension
|
|
1363
|
+
ext = content_model.extension
|
|
1364
|
+
return ext.base if ext.respond_to?(:base)
|
|
1365
|
+
elsif content_model.respond_to?(:restriction) && content_model.restriction
|
|
1366
|
+
rst = content_model.restriction
|
|
1367
|
+
return rst.base if rst.respond_to?(:base)
|
|
1368
|
+
end
|
|
1369
|
+
nil
|
|
1370
|
+
end
|
|
1371
|
+
|
|
1178
1372
|
# Extract base type from complex type
|
|
1179
1373
|
#
|
|
1180
1374
|
# @param type [ComplexType] Complex type
|
|
1181
1375
|
# @return [String, nil] Base type name
|
|
1182
1376
|
def extract_base_type(type)
|
|
1183
|
-
# Check complex_content for extension or restriction
|
|
1184
1377
|
if type.respond_to?(:complex_content) && type.complex_content
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
return cc.extension.base if cc.extension.respond_to?(:base)
|
|
1188
|
-
elsif cc.respond_to?(:restriction) && cc.restriction
|
|
1189
|
-
return cc.restriction.base if cc.restriction.respond_to?(:base)
|
|
1190
|
-
end
|
|
1378
|
+
base = base_from_content_model(type.complex_content)
|
|
1379
|
+
return base if base
|
|
1191
1380
|
end
|
|
1192
1381
|
|
|
1193
|
-
# Check simple_content for extension or restriction
|
|
1194
1382
|
if type.respond_to?(:simple_content) && type.simple_content
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
return sc.extension.base if sc.extension.respond_to?(:base)
|
|
1198
|
-
elsif sc.respond_to?(:restriction) && sc.restriction
|
|
1199
|
-
return sc.restriction.base if sc.restriction.respond_to?(:base)
|
|
1200
|
-
end
|
|
1383
|
+
base = base_from_content_model(type.simple_content)
|
|
1384
|
+
return base if base
|
|
1201
1385
|
end
|
|
1202
1386
|
|
|
1203
1387
|
nil
|
|
@@ -1459,17 +1643,17 @@ source = nil)
|
|
|
1459
1643
|
schema
|
|
1460
1644
|
end
|
|
1461
1645
|
|
|
1462
|
-
# Generate SVG diagram for a component using xsdvi
|
|
1646
|
+
# Generate SVG diagram for a component using xsdvi Ruby API
|
|
1463
1647
|
#
|
|
1464
1648
|
# @param component_data [Hash] Serialized component data
|
|
1465
1649
|
# @param component_type [Symbol] Component type (:element or :type)
|
|
1466
1650
|
# @param file_path [String, nil] XSD file path for xsdvi
|
|
1467
1651
|
# @return [String, nil] SVG diagram markup
|
|
1468
|
-
def generate_diagram(component_data,
|
|
1652
|
+
def generate_diagram(component_data, component_type, file_path = nil)
|
|
1469
1653
|
name = component_data[:name] || component_data["name"]
|
|
1470
1654
|
return nil unless name && file_path
|
|
1471
1655
|
|
|
1472
|
-
gen_element_diagram(name, file_path)
|
|
1656
|
+
gen_element_diagram(name, file_path, component_type)
|
|
1473
1657
|
rescue StandardError => e
|
|
1474
1658
|
warn "Warning: Failed to generate SVG diagram: #{e.message}" if ENV["DEBUG"]
|
|
1475
1659
|
nil
|
data/lib/lutaml/xsd/version.rb
CHANGED
data/lutaml-xsd.gemspec
CHANGED
|
@@ -31,7 +31,7 @@ Gem::Specification.new do |spec|
|
|
|
31
31
|
end + Dir.glob("frontend/dist/*")
|
|
32
32
|
end
|
|
33
33
|
|
|
34
|
-
spec.add_dependency "liquid", "
|
|
34
|
+
spec.add_dependency "liquid", ">= 4.0", "< 6.0"
|
|
35
35
|
spec.add_dependency "lutaml-model", "~> 0.8.0"
|
|
36
36
|
spec.add_dependency "moxml"
|
|
37
37
|
spec.add_dependency "paint", "~> 2.3"
|
metadata
CHANGED
|
@@ -1,29 +1,35 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: lutaml-xsd
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.1.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-05-06 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: liquid
|
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
|
16
16
|
requirements:
|
|
17
|
-
- - "
|
|
17
|
+
- - ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '4.0'
|
|
20
|
+
- - "<"
|
|
18
21
|
- !ruby/object:Gem::Version
|
|
19
|
-
version: '
|
|
22
|
+
version: '6.0'
|
|
20
23
|
type: :runtime
|
|
21
24
|
prerelease: false
|
|
22
25
|
version_requirements: !ruby/object:Gem::Requirement
|
|
23
26
|
requirements:
|
|
24
|
-
- - "
|
|
27
|
+
- - ">="
|
|
28
|
+
- !ruby/object:Gem::Version
|
|
29
|
+
version: '4.0'
|
|
30
|
+
- - "<"
|
|
25
31
|
- !ruby/object:Gem::Version
|
|
26
|
-
version: '
|
|
32
|
+
version: '6.0'
|
|
27
33
|
- !ruby/object:Gem::Dependency
|
|
28
34
|
name: lutaml-model
|
|
29
35
|
requirement: !ruby/object:Gem::Requirement
|