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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e2649418d3ea00927ab8b73628856fa55ed114ca61489f6834b6d6daca9d416b
4
- data.tar.gz: 16452655b05b900cb227ea65d63b555d593cd8e31ffceeb8af78872e4ff7a94b
3
+ metadata.gz: 29e9ba5ed3edf116b76cdf22ed853a37399d00ffda6b11f78a0cccdb5502b4e4
4
+ data.tar.gz: 6213f84485cad0ffed1eef19d60ae29fea7d5e274522ba8dfcc996048694e677
5
5
  SHA512:
6
- metadata.gz: ae2220574c0cd979eb05eed45efe8770ba855cf641d6b30d68f899f370a831598392e44e7ab81e6a85d3c5e4c975c24057a2c11e783757d4831e23a8aae6435a
7
- data.tar.gz: 4773bd5fc5b44b98d4ca7b2cffc60a233abd60c221829c77611d002e03f07c4de9142c1731ffa58827e22cac0f1a606548d109e08a2466073bece6093fe56ce8
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-25 09:44:01 UTC using RuboCop version 1.86.1.
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: keyword, variable, start_of_line
41
- Layout/EndAlignment:
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
- # Configuration parameters: EnforcedStyle, IndentationWidth.
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: 1
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: 730
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: 7
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: AllowInHeredoc.
89
- Layout/TrailingWhitespace:
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: 267
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: 199
100
+ # Offense count: 201
146
101
  # Configuration parameters: AllowedMethods, AllowedPatterns, Max.
147
102
  Metrics/CyclomaticComplexity:
148
103
  Enabled: false
149
104
 
150
- # Offense count: 370
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: 149
115
+ # Offense count: 151
161
116
  # Configuration parameters: AllowedMethods, AllowedPatterns, Max.
162
117
  Metrics/PerceivedComplexity:
163
118
  Enabled: false
164
119
 
165
- # Offense count: 24
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
- output_folder = Dir.mktmpdir("xsdvi-")
465
+ actual_file_path = file_path
466
+ wrapper_name = name
365
467
 
366
- # generate diagram using xsdvi command line tool
367
- `bundle exec xsdvi generate #{file_path} -r #{name} -o -p #{output_folder}`
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
- unless File.exist?(svg_file)
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 information for an attribute group
646
- # from the schema
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}='#{value}']"
863
+ "//#{prefix}:#{type}[@#{key}='#{escaped_value}']"
656
864
  else
657
- "//#{type}[@#{key}='#{value}']"
865
+ "//#{type}[@#{key}='#{escaped_value}']"
658
866
  end
659
- ag_node = doc.at_xpath(xpath)
660
- ag_node&.to_xml(indent: 2)
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
- # Collect attribute group refs from all three possible locations:
794
- # 1. Direct attribute groups on the type
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
- sc = type.simple_content
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
- cc = type.complex_content
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
- facets = []
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
- if restriction.respond_to?(:min_length) && restriction.min_length
1097
- facets << { type: "min_length",
1098
- value: restriction.min_length }
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
- if restriction.respond_to?(:max_length) && restriction.max_length
1102
- facets << { type: "max_length",
1103
- value: restriction.max_length }
1104
- end
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
- cc = type.complex_content
1186
- if cc.respond_to?(:extension) && cc.extension
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
- sc = type.simple_content
1196
- if sc.respond_to?(:extension) && sc.extension
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 CLI
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, _component_type, file_path = nil)
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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Lutaml
4
4
  module Xsd
5
- VERSION = "1.0.9"
5
+ VERSION = "1.1.1"
6
6
  end
7
7
  end
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", "~> 5.0"
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.0.9
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-04-27 00:00:00.000000000 Z
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: '5.0'
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: '5.0'
32
+ version: '6.0'
27
33
  - !ruby/object:Gem::Dependency
28
34
  name: lutaml-model
29
35
  requirement: !ruby/object:Gem::Requirement