lutaml-model 0.8.17 → 0.8.18

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 (192) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/downstream-performance.yml +27 -1
  3. data/.github/workflows/js-pr-check.yml +102 -0
  4. data/.github/workflows/js-sync.yml +51 -0
  5. data/.github/workflows/opal.yml +26 -6
  6. data/.github/workflows/performance.yml +32 -7
  7. data/.github/workflows/rake.yml +78 -32
  8. data/.gitmodules +6 -0
  9. data/.rubocop_todo.yml +8 -0
  10. data/Gemfile +16 -1
  11. data/README.adoc +267 -0
  12. data/Rakefile +166 -4
  13. data/docs/_guides/schema-import.adoc +42 -0
  14. data/docs/_guides/xml-mapping.adoc +21 -0
  15. data/docs/_pages/validation.adoc +65 -0
  16. data/lib/compat/opal/generate_boot.rb +123 -0
  17. data/lib/compat/opal/js_bundle_entry.rb +42 -0
  18. data/lib/compat/opal/lutaml_model_boot.rb +497 -0
  19. data/lib/compat/opal/moxml_boot.rb +68 -0
  20. data/lib/compat/opal/yaml_compat.rb +32 -0
  21. data/lib/lutaml/json.rb +1 -1
  22. data/lib/lutaml/key_value/transform.rb +3 -8
  23. data/lib/lutaml/key_value/transformation/value_serializer.rb +14 -1
  24. data/lib/lutaml/key_value/transformation.rb +17 -5
  25. data/lib/lutaml/model/adapter_resolver.rb +2 -2
  26. data/lib/lutaml/model/attribute.rb +88 -11
  27. data/lib/lutaml/model/cached_type_resolver.rb +1 -1
  28. data/lib/lutaml/model/error/union_schema_unsupported_error.rb +11 -0
  29. data/lib/lutaml/model/global_context.rb +11 -8
  30. data/lib/lutaml/model/instrumentation.rb +2 -2
  31. data/lib/lutaml/model/mapping/mapping_rule.rb +0 -6
  32. data/lib/lutaml/model/register.rb +2 -2
  33. data/lib/lutaml/model/runtime_compatibility.rb +50 -0
  34. data/lib/lutaml/model/schema/class_loader.rb +58 -0
  35. data/lib/lutaml/model/schema/compiled_output.rb +83 -0
  36. data/lib/lutaml/model/schema/definitions/attribute.rb +32 -0
  37. data/lib/lutaml/model/schema/definitions/choice.rb +20 -0
  38. data/lib/lutaml/model/schema/definitions/facet.rb +40 -0
  39. data/lib/lutaml/model/schema/definitions/group_import.rb +22 -0
  40. data/lib/lutaml/model/schema/definitions/member_walk.rb +33 -0
  41. data/lib/lutaml/model/schema/definitions/model.rb +40 -0
  42. data/lib/lutaml/model/schema/definitions/namespace.rb +23 -0
  43. data/lib/lutaml/model/schema/definitions/restricted_type.rb +30 -0
  44. data/lib/lutaml/model/schema/definitions/sequence.rb +19 -0
  45. data/lib/lutaml/model/schema/definitions/simple_content.rb +22 -0
  46. data/lib/lutaml/model/schema/definitions/transform_facet.rb +21 -0
  47. data/lib/lutaml/model/schema/definitions/type_ref.rb +20 -0
  48. data/lib/lutaml/model/schema/definitions/union_type.rb +28 -0
  49. data/lib/lutaml/model/schema/definitions/xml_root.rb +20 -0
  50. data/lib/lutaml/model/schema/definitions.rb +24 -0
  51. data/lib/lutaml/model/schema/file_writer.rb +70 -0
  52. data/lib/lutaml/model/schema/generator/definitions_collection.rb +16 -2
  53. data/lib/lutaml/model/schema/generator/property.rb +21 -4
  54. data/lib/lutaml/model/schema/module_nesting.rb +31 -0
  55. data/lib/lutaml/model/schema/namespace_naming.rb +46 -0
  56. data/lib/lutaml/model/schema/registry_generator.rb +115 -0
  57. data/lib/lutaml/model/schema/renderers/base.rb +40 -0
  58. data/lib/lutaml/model/schema/renderers/mappings.rb +72 -0
  59. data/lib/lutaml/model/schema/renderers/member_decls.rb +115 -0
  60. data/lib/lutaml/model/schema/renderers/model.rb +121 -0
  61. data/lib/lutaml/model/schema/renderers/namespace.rb +30 -0
  62. data/lib/lutaml/model/schema/renderers/registration.rb +64 -0
  63. data/lib/lutaml/model/schema/renderers/required_files_calculator.rb +93 -0
  64. data/lib/lutaml/model/schema/renderers/restricted_type.rb +88 -0
  65. data/lib/lutaml/model/schema/renderers/union.rb +83 -0
  66. data/lib/lutaml/model/schema/renderers.rb +19 -0
  67. data/lib/lutaml/model/schema/rnc_compiler/source_resolver.rb +47 -0
  68. data/lib/lutaml/model/schema/rnc_compiler.rb +62 -0
  69. data/lib/lutaml/model/schema/rng_compiler/define_classifier.rb +91 -0
  70. data/lib/lutaml/model/schema/rng_compiler/element_visitor.rb +392 -0
  71. data/lib/lutaml/model/schema/rng_compiler/member_collector.rb +30 -0
  72. data/lib/lutaml/model/schema/rng_compiler/rng_helpers.rb +152 -0
  73. data/lib/lutaml/model/schema/rng_compiler/value_type_resolver.rb +125 -0
  74. data/lib/lutaml/model/schema/rng_compiler.rb +220 -0
  75. data/lib/lutaml/model/schema/templates.rb +162 -0
  76. data/lib/lutaml/model/schema/xml_compiler/element_order.rb +41 -0
  77. data/lib/lutaml/model/schema/xml_compiler/registry_generator.rb +31 -107
  78. data/lib/lutaml/model/schema/xml_compiler/spec_builder/complex_types.rb +191 -0
  79. data/lib/lutaml/model/schema/xml_compiler/spec_builder/members.rb +216 -0
  80. data/lib/lutaml/model/schema/xml_compiler/spec_builder/simple_types.rb +153 -0
  81. data/lib/lutaml/model/schema/xml_compiler/spec_builder.rb +167 -0
  82. data/lib/lutaml/model/schema/xml_compiler/supported_data_types.rb +87 -0
  83. data/lib/lutaml/model/schema/xml_compiler.rb +38 -517
  84. data/lib/lutaml/model/schema.rb +19 -0
  85. data/lib/lutaml/model/serialize/attribute_definition.rb +14 -2
  86. data/lib/lutaml/model/serialize/builder.rb +15 -2
  87. data/lib/lutaml/model/serialize/value_mapping.rb +9 -57
  88. data/lib/lutaml/model/serialize.rb +6 -9
  89. data/lib/lutaml/model/services/rule_value_extractor.rb +2 -3
  90. data/lib/lutaml/model/services.rb +0 -2
  91. data/lib/lutaml/model/store.rb +2 -1
  92. data/lib/lutaml/model/toml.rb +1 -1
  93. data/lib/lutaml/model/transform.rb +17 -39
  94. data/lib/lutaml/model/transformation_registry.rb +1 -1
  95. data/lib/lutaml/model/type.rb +1 -0
  96. data/lib/lutaml/model/union.rb +320 -0
  97. data/lib/lutaml/model/utils.rb +6 -0
  98. data/lib/lutaml/model/validation.rb +1 -4
  99. data/lib/lutaml/model/version.rb +1 -1
  100. data/lib/lutaml/model.rb +18 -0
  101. data/lib/lutaml/toml.rb +1 -1
  102. data/lib/lutaml/xml/adapter.rb +1 -1
  103. data/lib/lutaml/xml/adapter_loader.rb +1 -1
  104. data/lib/lutaml/xml/builder/base.rb +1 -1
  105. data/lib/lutaml/xml/builder.rb +1 -1
  106. data/lib/lutaml/xml/error/schema_validation_error.rb +25 -0
  107. data/lib/lutaml/xml/model_transform.rb +5 -1
  108. data/lib/lutaml/xml/schema/relaxng_schema.rb +5 -0
  109. data/lib/lutaml/xml/schema/xsd_schema.rb +4 -0
  110. data/lib/lutaml/xml/serialization/format_conversion.rb +48 -0
  111. data/lib/lutaml/xml/serialization/instance_methods.rb +13 -0
  112. data/lib/lutaml/xml/transformation/element_builder.rb +21 -8
  113. data/lib/lutaml/xml/transformation/ordered_applier.rb +6 -0
  114. data/lib/lutaml/xml/xsd_validator.rb +66 -0
  115. data/lib/lutaml/xml.rb +53 -12
  116. data/lutaml-model.gemspec +4 -2
  117. data/spec/fixtures/xml/schema/rnc/address_book.rnc +15 -0
  118. data/spec/fixtures/xml/schema/rnc/book_features.rnc +18 -0
  119. data/spec/fixtures/xml/schema/rnc/includes/book.rnc +6 -0
  120. data/spec/fixtures/xml/schema/rnc/includes/circular_a.rnc +8 -0
  121. data/spec/fixtures/xml/schema/rnc/includes/circular_b.rnc +8 -0
  122. data/spec/fixtures/xml/schema/rnc/includes/library.rnc +8 -0
  123. data/spec/fixtures/xml/schema/rng/address_book.rng +16 -0
  124. data/spec/fixtures/xml/schema/rng/book.rng +27 -0
  125. data/spec/fixtures/xml/schema/rng/fixed_value.rng +11 -0
  126. data/spec/fixtures/xml/schema/rng/fragment_in_choice.rng +30 -0
  127. data/spec/fixtures/xml/schema/rng/full_name.rng +39 -0
  128. data/spec/fixtures/xml/schema/rng/integer_range.rng +32 -0
  129. data/spec/fixtures/xml/schema/rng/list_type.rng +12 -0
  130. data/spec/fixtures/xml/schema/rng/namespaced.rng +9 -0
  131. data/spec/fixtures/xml/schema/rng/paragraph.rng +13 -0
  132. data/spec/fixtures/xml/schema/rng/person.rng +23 -0
  133. data/spec/fixtures/xml/schema/rng/union.rng +14 -0
  134. data/spec/fixtures/xml/validate_xml_with_address.xsd +13 -0
  135. data/spec/fixtures/xml/validate_xml_with_contact.xsd +11 -0
  136. data/spec/fixtures/xml/validate_xml_with_person.xsd +11 -0
  137. data/spec/fixtures/xml/validate_xml_with_person_strict.xsd +17 -0
  138. data/spec/fixtures/xml/validate_xml_with_product.xsd +37 -0
  139. data/spec/lutaml/model/attribute_apply_value_map_spec.rb +134 -0
  140. data/spec/lutaml/model/attribute_spec.rb +169 -12
  141. data/spec/lutaml/model/boot_manifest_spec.rb +30 -0
  142. data/spec/lutaml/model/collection_spec.rb +18 -0
  143. data/spec/lutaml/model/mixed_content_spec.rb +171 -0
  144. data/spec/lutaml/model/opal_smoke_spec.rb +67 -2
  145. data/spec/lutaml/model/ordered_content_spec.rb +30 -0
  146. data/spec/lutaml/model/raw_element_spec.rb +125 -0
  147. data/spec/lutaml/model/rule_value_extractor_spec.rb +7 -14
  148. data/spec/lutaml/model/runtime_compatibility_spec.rb +25 -0
  149. data/spec/lutaml/model/schema/renderers/model_spec.rb +149 -0
  150. data/spec/lutaml/model/schema/renderers/namespace_spec.rb +36 -0
  151. data/spec/lutaml/model/schema/renderers/restricted_type_spec.rb +75 -0
  152. data/spec/lutaml/model/schema/renderers/union_spec.rb +58 -0
  153. data/spec/lutaml/model/transform_apply_value_map_spec.rb +64 -0
  154. data/spec/lutaml/model/union_attribute_spec.rb +658 -0
  155. data/spec/lutaml/model/union_spec.rb +257 -0
  156. data/spec/lutaml/model/utils_spec.rb +14 -0
  157. data/spec/lutaml/xml/opal_xml_spec.rb +22 -20
  158. data/spec/lutaml/xml/schema/compiler_spec.rb +21 -4
  159. data/spec/lutaml/xml/schema/rnc_compiler_spec.rb +404 -0
  160. data/spec/lutaml/xml/schema/rng_compiler_spec.rb +893 -0
  161. data/spec/lutaml/xml/validate_xml_with_constructs_spec.rb +222 -0
  162. data/spec/lutaml/xml/validate_xml_with_spec.rb +186 -0
  163. data/spec/spec_helper.rb +28 -11
  164. data/spec/support/opal.rb +5 -2
  165. metadata +97 -31
  166. data/lib/lutaml/model/schema/xml_compiler/attribute.rb +0 -106
  167. data/lib/lutaml/model/schema/xml_compiler/attribute_group.rb +0 -47
  168. data/lib/lutaml/model/schema/xml_compiler/choice.rb +0 -67
  169. data/lib/lutaml/model/schema/xml_compiler/complex_content.rb +0 -27
  170. data/lib/lutaml/model/schema/xml_compiler/complex_content_restriction.rb +0 -34
  171. data/lib/lutaml/model/schema/xml_compiler/complex_type.rb +0 -173
  172. data/lib/lutaml/model/schema/xml_compiler/element.rb +0 -121
  173. data/lib/lutaml/model/schema/xml_compiler/group.rb +0 -113
  174. data/lib/lutaml/model/schema/xml_compiler/restriction.rb +0 -104
  175. data/lib/lutaml/model/schema/xml_compiler/sequence.rb +0 -52
  176. data/lib/lutaml/model/schema/xml_compiler/simple_content.rb +0 -40
  177. data/lib/lutaml/model/schema/xml_compiler/simple_type.rb +0 -265
  178. data/lib/lutaml/model/schema/xml_compiler/xml_namespace_class.rb +0 -110
  179. data/lib/lutaml/model/services/default_value_resolver.rb +0 -60
  180. data/spec/lutaml/model/services/default_value_resolver_spec.rb +0 -162
  181. data/spec/lutaml/xml/schema/compiler/attribute_group_spec.rb +0 -71
  182. data/spec/lutaml/xml/schema/compiler/attribute_spec.rb +0 -67
  183. data/spec/lutaml/xml/schema/compiler/choice_spec.rb +0 -74
  184. data/spec/lutaml/xml/schema/compiler/complex_content_restriction_spec.rb +0 -60
  185. data/spec/lutaml/xml/schema/compiler/complex_content_spec.rb +0 -41
  186. data/spec/lutaml/xml/schema/compiler/complex_type_spec.rb +0 -202
  187. data/spec/lutaml/xml/schema/compiler/element_spec.rb +0 -68
  188. data/spec/lutaml/xml/schema/compiler/group_spec.rb +0 -90
  189. data/spec/lutaml/xml/schema/compiler/restriction_spec.rb +0 -77
  190. data/spec/lutaml/xml/schema/compiler/sequence_spec.rb +0 -65
  191. data/spec/lutaml/xml/schema/compiler/simple_content_spec.rb +0 -61
  192. data/spec/lutaml/xml/schema/compiler/simple_type_spec.rb +0 -184
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uri"
4
+
5
+ module Lutaml
6
+ module Model
7
+ module Schema
8
+ # Derives the class name and default prefix for a generated
9
+ # XmlNamespace subclass from a namespace URI.
10
+ #
11
+ # Used by both XSD and RNG compilers so the same URI always
12
+ # produces the same class name across formats.
13
+ module NamespaceNaming
14
+ module_function
15
+
16
+ def prefix_for(uri)
17
+ case uri
18
+ when %r{/math$} then "m"
19
+ when %r{XMLSchema} then "xs"
20
+ when %r{/(\w+)\z} then ::Regexp.last_match(1)[0..2]
21
+ else "ns"
22
+ end
23
+ end
24
+
25
+ def class_name_for(uri)
26
+ return "XmlSchemaNamespace" if uri.include?("XMLSchema")
27
+
28
+ parsed = URI.parse(uri)
29
+ host_parts = (parsed.host&.split(".") || []).reject(&:empty?)
30
+ path_parts = (parsed.path&.split("/") || []).reject(&:empty?)
31
+
32
+ name_parts = host_parts.reverse.take(2) + path_parts.last(2)
33
+ name_parts = name_parts.map do |p|
34
+ Utils.camel_case(p.gsub(/\d+/, "").gsub(/[^a-zA-Z]/, ""))
35
+ end.reject(&:empty?)
36
+
37
+ return "DefaultNamespace" if name_parts.empty?
38
+
39
+ "#{name_parts.join}Namespace"
40
+ rescue URI::InvalidURIError
41
+ "DefaultNamespace"
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "erb"
4
+
5
+ module Lutaml
6
+ module Model
7
+ module Schema
8
+ # Base class for per-format registry generators (one Ruby file that
9
+ # declares autoloads + a `self.register_all` that registers every
10
+ # generated class with the model registry).
11
+ #
12
+ # Subclasses customize the ERB skeleton (e.g., XSD adds import
13
+ # resolution phases) but inherit the shared module-wrap and
14
+ # autoload/registration emit methods.
15
+ class RegistryGenerator
16
+ DEFAULT_TEMPLATE = ERB.new(<<~TMPL, trim_mode: "-")
17
+ # frozen_string_literal: true
18
+ # Auto-generated central registry for <%= @module_namespace %>
19
+
20
+ <%= module_opening -%>
21
+ <%= autoload_declarations %>
22
+
23
+ def self.register_all
24
+ context = Lutaml::Model::GlobalContext.context(:<%= @register_id %>) ||
25
+ Lutaml::Model::GlobalContext.create_context(id: :<%= @register_id %>)
26
+
27
+ <%= registration_body %>
28
+ end
29
+ <%= module_closing -%>
30
+ TMPL
31
+
32
+ def self.generate(model_entries, options = {})
33
+ new(model_entries, options).generate
34
+ end
35
+
36
+ # `model_entries` is an Array of CompiledOutput::Entry with kind
37
+ # `:model`. `options[:namespaces]` is an Array of `:namespace`
38
+ # entries, autoloaded but never registered.
39
+ def initialize(model_entries, options)
40
+ @classes = model_entries
41
+ @namespaces = options[:namespaces] || []
42
+ @module_namespace = options[:module_namespace]
43
+ @register_id = options[:register_id] || :default
44
+ @modules = @module_namespace&.split("::") || []
45
+ end
46
+
47
+ def generate
48
+ return nil unless @module_namespace
49
+
50
+ template.result(binding)
51
+ end
52
+
53
+ private
54
+
55
+ # Subclasses can override to use a different ERB skeleton.
56
+ def template
57
+ self.class::DEFAULT_TEMPLATE
58
+ end
59
+
60
+ # The body of `def self.register_all`. Default is just the
61
+ # registration block; subclasses can override to add extra phases
62
+ # (e.g., XSD's ensure_imports! / ensure_mappings_imported! passes).
63
+ def registration_body
64
+ registration_calls
65
+ end
66
+
67
+ # Inside-`register_all` indent, used by phase-comment overrides.
68
+ def body_indent
69
+ " " * (module_depth + 1)
70
+ end
71
+
72
+ # Autoload every generated constant: model classes AND namespace
73
+ # classes. Models are then registered with `register_all`;
74
+ # namespaces are only autoloaded (model classes reference them
75
+ # in their `namespace ...` mapping declarations).
76
+ def autoload_declarations
77
+ indent = " " * module_depth
78
+ module_subdir = @module_namespace.split("::").last.downcase
79
+ (@classes + @namespaces).map do |entry|
80
+ class_name = Utils.camel_case(entry.name)
81
+ file_name = Utils.snake_case(entry.name)
82
+ "#{indent}autoload :#{class_name}, " \
83
+ "File.join(__dir__, \"#{module_subdir}\", \"#{file_name}\")"
84
+ end.join("\n")
85
+ end
86
+
87
+ def registration_calls
88
+ indent = " " * (module_depth + 1)
89
+ @classes.map do |entry|
90
+ class_name = Utils.camel_case(entry.name)
91
+ id = Utils.snake_case(entry.name).to_sym
92
+ "#{indent}#{class_name} # Trigger autoload\n" \
93
+ "#{indent}context.registry.register(:#{id}, #{class_name})\n" \
94
+ "#{indent}#{class_name}.instance_variable_set(:@register, :#{@register_id})"
95
+ end.join("\n")
96
+ end
97
+
98
+ # Thin forwarders so the ERB template using `<%= module_opening %>`
99
+ # / `<%= module_closing %>` keeps working. Logic lives in
100
+ # `Lutaml::Model::Schema::ModuleNesting`.
101
+ def module_opening
102
+ ModuleNesting.opening(Array(@modules))
103
+ end
104
+
105
+ def module_closing
106
+ ModuleNesting.closing(Array(@modules))
107
+ end
108
+
109
+ def module_depth
110
+ @modules.size
111
+ end
112
+ end
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "erb"
4
+
5
+ module Lutaml
6
+ module Model
7
+ module Schema
8
+ module Renderers
9
+ # Shared constructor and helpers for the value/namespace renderers
10
+ # (Union, RestrictedType, Namespace). Model has its own constructor
11
+ # because it computes an opt-out (module-wrappable) namespace.
12
+ class Base
13
+ def self.render(spec, **)
14
+ new(spec, **).render
15
+ end
16
+
17
+ def initialize(spec, indent: 2, module_namespace: nil, register_id: :default)
18
+ @spec = spec
19
+ @indent = indent.is_a?(Integer) ? " " * indent : indent
20
+ @extended_indent = @indent * 2
21
+ @module_namespace = module_namespace
22
+ @modules = module_namespace&.split("::") || []
23
+ @register_id = register_id
24
+ end
25
+
26
+ private
27
+
28
+ def required_files_block
29
+ files = @spec.required_files
30
+ files.empty? ? "" : "#{files.uniq.join("\n")}\n"
31
+ end
32
+
33
+ def module_opening = ModuleNesting.opening(@modules)
34
+ def module_closing = ModuleNesting.closing(@modules)
35
+ def boilerplate_indent_str = @indent
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Model
5
+ module Schema
6
+ module Renderers
7
+ # Renders the `map_element` / `map_attribute` / `sequence do` lines
8
+ # inside the `xml do ... end` block of a generated class. Used by
9
+ # Renderers::Model.
10
+ class Mappings
11
+ def self.render(members, indent:, base_indent:, simple_content: nil)
12
+ new(indent: indent, base_indent: base_indent,
13
+ simple_content: simple_content).render(members)
14
+ end
15
+
16
+ def initialize(indent:, base_indent:, simple_content:)
17
+ @indent = indent
18
+ @base_indent = base_indent
19
+ @simple_content = simple_content
20
+ end
21
+
22
+ def render(members)
23
+ members.map { |m| render_one(m, @indent) }.join +
24
+ simple_content_attribute_mappings
25
+ end
26
+
27
+ private
28
+
29
+ def render_one(member, indent)
30
+ case member
31
+ when Definitions::Sequence then render_sequence(member, indent)
32
+ when Definitions::Choice then member.alternatives.map { |a| render_one(a, indent) }.join
33
+ when Definitions::Attribute then render_attribute(member, indent)
34
+ when Definitions::GroupImport then "#{indent}import_model_mappings :#{member.name}\n"
35
+ end
36
+ end
37
+
38
+ def render_sequence(sequence, indent)
39
+ inner_indent = indent + @base_indent
40
+ inner = sequence.members.map { |m| render_one(m, inner_indent) }.join
41
+ "#{indent}sequence do\n#{inner}#{indent}end\n"
42
+ end
43
+
44
+ def render_attribute(attr, indent)
45
+ case attr.kind
46
+ when :element then map_member("map_element", attr, indent)
47
+ when :attribute then map_member("map_attribute", attr, indent)
48
+ else ""
49
+ end
50
+ end
51
+
52
+ def map_member(verb, attr, indent)
53
+ %(#{indent}#{verb} "#{attr.xml_name}", to: :#{attr.name}#{render_options(attr)}\n)
54
+ end
55
+
56
+ def render_options(attr)
57
+ opts = []
58
+ opts << "render_default: true" if attr.render_default
59
+ opts << "render_empty: true" if attr.render_empty
60
+ opts.empty? ? "" : ", #{opts.join(', ')}"
61
+ end
62
+
63
+ def simple_content_attribute_mappings
64
+ return "" unless @simple_content
65
+
66
+ @simple_content.additional_attributes.map { |a| render_one(a, @indent) }.join
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Model
5
+ module Schema
6
+ module Renderers
7
+ # Renders the attribute-declaration block of a generated class:
8
+ # the lines between `class X < Parent` and `xml do`. Used by
9
+ # Renderers::Model.
10
+ class MemberDecls
11
+ def self.render(members, indent:, base_indent:, mixed: false,
12
+ text_content: false, simple_content: nil)
13
+ new(indent: indent, base_indent: base_indent,
14
+ mixed: mixed,
15
+ text_content: text_content,
16
+ simple_content: simple_content).render(members)
17
+ end
18
+
19
+ def initialize(indent:, base_indent:, mixed:, text_content:,
20
+ simple_content:)
21
+ @indent = indent
22
+ @base_indent = base_indent
23
+ @mixed = mixed
24
+ @text_content = text_content
25
+ @simple_content = simple_content
26
+ end
27
+
28
+ def render(members)
29
+ lines = members.map { |m| render_one(m, @indent) }.join
30
+ lines + content_attribute_line + simple_content_extras
31
+ end
32
+
33
+ private
34
+
35
+ def render_one(member, indent)
36
+ case member
37
+ when Definitions::Choice then render_choice(member, indent)
38
+ when Definitions::Sequence then member.members.map { |m| render_one(m, indent) }.join
39
+ when Definitions::Attribute then render_attribute(member, indent)
40
+ when Definitions::GroupImport then "#{indent}import_model_attributes :#{member.name}\n"
41
+ end
42
+ end
43
+
44
+ def render_choice(choice, indent)
45
+ inner_indent = indent + @base_indent
46
+ inner = choice.alternatives.map { |alt| render_one(alt, inner_indent) }.join
47
+ "#{indent}#{choice.header} do\n#{inner}#{indent}end\n"
48
+ end
49
+
50
+ def render_attribute(attr, indent)
51
+ doc = format_doc(attr.documentation, indent)
52
+ "#{doc}#{indent}attribute :#{attr.name}, #{type_literal(attr.type)}#{options_suffix(attr)}\n"
53
+ end
54
+
55
+ def format_doc(doc, indent)
56
+ return "" if Utils.blank?(doc&.to_s&.strip)
57
+
58
+ doc.to_s.lines.map { |line| "#{indent}# #{line.strip}\n" }.join
59
+ end
60
+
61
+ def type_literal(type_ref)
62
+ case type_ref.kind
63
+ when :symbol then ":#{type_ref.value}"
64
+ when :class_ref then type_ref.value
65
+ when :w3c then "::#{type_ref.value}"
66
+ end
67
+ end
68
+
69
+ def options_suffix(attr)
70
+ opts = []
71
+ opts << collection_option(attr.collection) if attr.collection
72
+ opts << "default: -> { #{attr.default.inspect} }" if attr.default
73
+ opts << "initialize_empty: true" if attr.initialize_empty
74
+ opts.empty? ? "" : ", #{opts.join(', ')}"
75
+ end
76
+
77
+ def collection_option(coll)
78
+ case coll
79
+ when true then "collection: true"
80
+ when Range then range_option(coll)
81
+ end
82
+ end
83
+
84
+ def range_option(range)
85
+ ending = if range.end.respond_to?(:infinite?) && range.end.infinite?
86
+ "Float::INFINITY"
87
+ else
88
+ range.end
89
+ end
90
+ "collection: #{range.begin}..#{ending}"
91
+ end
92
+
93
+ def content_attribute_line
94
+ return simple_content_attribute if @simple_content
95
+ return "#{@indent}attribute :content, :string, collection: true\n" if @mixed
96
+ return "#{@indent}attribute :content, :string\n" if @text_content
97
+
98
+ ""
99
+ end
100
+
101
+ def simple_content_attribute
102
+ type_sym = Utils.snake_case(Utils.last_of_split(@simple_content.base_class))
103
+ "#{@indent}attribute :content, :#{type_sym}\n"
104
+ end
105
+
106
+ def simple_content_extras
107
+ return "" unless @simple_content
108
+
109
+ @simple_content.additional_attributes.map { |a| render_attribute(a, @indent) }.join
110
+ end
111
+ end
112
+ end
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,121 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "erb"
4
+
5
+ module Lutaml
6
+ module Model
7
+ module Schema
8
+ module Renderers
9
+ # Renders a Definitions::Model into a Lutaml::Model::Serializable
10
+ # subclass as Ruby source. Member-declaration and xml-mapping
11
+ # rendering is delegated to MemberDecls and Mappings.
12
+ class Model
13
+ def self.render(spec, **)
14
+ new(spec, **).render
15
+ end
16
+
17
+ def initialize(spec, indent: 2, module_namespace: nil, register_id: :default)
18
+ @spec = spec
19
+ @indent = indent.is_a?(Integer) ? " " * indent : indent
20
+ @extended_indent = @indent * 2
21
+ @raw_module_namespace = module_namespace
22
+ @register_id = register_id
23
+ end
24
+
25
+ def render
26
+ Templates::SERIALIZABLE_CLASS.result(binding)
27
+ end
28
+
29
+ private
30
+
31
+ # Effective namespace (nil when spec opts out of module wrapping).
32
+ def module_namespace
33
+ @spec.module_wrappable ? @raw_module_namespace : nil
34
+ end
35
+
36
+ def modules
37
+ module_namespace&.split("::") || []
38
+ end
39
+
40
+ def rendered_class_name = @spec.class_name
41
+ def serializable_class_parent = @spec.parent_class
42
+ def serializable_class_required_files = format_files(@spec.required_files)
43
+ def serializable_class_documentation = format_doc_block(@spec.documentation)
44
+ def serializable_class_imports = render_imports
45
+
46
+ def serializable_class_attributes
47
+ MemberDecls.render(
48
+ @spec.members,
49
+ indent: @indent,
50
+ base_indent: @indent,
51
+ mixed: @spec.mixed,
52
+ text_content: @spec.text_content,
53
+ simple_content: @spec.simple_content,
54
+ )
55
+ end
56
+
57
+ def xml_attribute_mappings
58
+ Mappings.render(
59
+ @spec.members,
60
+ indent: @extended_indent,
61
+ base_indent: @indent,
62
+ simple_content: @spec.simple_content,
63
+ )
64
+ end
65
+
66
+ def xml_root_directive_line
67
+ case @spec.xml_root.kind
68
+ when :element then %(element "#{@spec.xml_root.name}")
69
+ when :type_name then %(type_name "#{@spec.xml_root.name}")
70
+ end
71
+ end
72
+
73
+ def xml_namespace_line
74
+ ns = @spec.namespace_class_name
75
+ ns && "namespace #{ns}"
76
+ end
77
+
78
+ def xml_mixed_content? = @spec.mixed
79
+ def xml_text_content? = @spec.text_content || !!@spec.simple_content || @spec.mixed
80
+ def xml_extra_mappings = ""
81
+
82
+ def module_opening = ModuleNesting.opening(modules)
83
+ def module_closing = ModuleNesting.closing(modules)
84
+ def boilerplate_indent_str = @indent
85
+
86
+ def registration_methods
87
+ Registration.methods_block(
88
+ class_name: @spec.class_name,
89
+ module_namespace: module_namespace,
90
+ indent: @indent,
91
+ lazy: @spec.lazy_register,
92
+ )
93
+ end
94
+
95
+ def registration_execution
96
+ Registration.execution_line(
97
+ class_name: @spec.class_name,
98
+ module_namespace: module_namespace,
99
+ )
100
+ end
101
+
102
+ def format_files(files)
103
+ return "" if files.empty? || module_namespace
104
+
105
+ "#{files.uniq.join("\n")}\n"
106
+ end
107
+
108
+ def format_doc_block(doc)
109
+ return "" if Utils.blank?(doc&.to_s&.strip)
110
+
111
+ doc.to_s.lines.map { |line| "# #{line.strip}\n" }.join
112
+ end
113
+
114
+ def render_imports
115
+ @spec.imports.map { |name| "#{@indent}import_model #{name}\n" }.join
116
+ end
117
+ end
118
+ end
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "erb"
4
+
5
+ module Lutaml
6
+ module Model
7
+ module Schema
8
+ module Renderers
9
+ # Renders a Definitions::Namespace into a
10
+ # Lutaml::Xml::W3c::XmlNamespace subclass.
11
+ class Namespace < Base
12
+ def render
13
+ Templates::XML_NAMESPACE.result(binding)
14
+ end
15
+
16
+ private
17
+
18
+ def class_name = @spec.class_name
19
+ def uri = @spec.uri
20
+ def prefix_default = @spec.prefix_default
21
+
22
+ def element_form_default_line
23
+ efd = @spec.element_form_default
24
+ efd && "element_form_default #{efd.inspect}"
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Model
5
+ module Schema
6
+ module Renderers
7
+ # Stateless helper for emitting the `self.register` and
8
+ # `self.register_class_with_id` method block plus the trailing
9
+ # execution line, shared by the Model, RestrictedType, and Union
10
+ # renderers.
11
+ #
12
+ # Cases:
13
+ # module_namespace = nil -> emit both methods + execution
14
+ # namespace + keep_when_namespaced -> emit only `self.register`
15
+ # namespace + !keep -> emit nothing
16
+ module Registration
17
+ module_function
18
+
19
+ def methods_block(class_name:, module_namespace:, indent:,
20
+ lazy: false, keep_when_namespaced: false)
21
+ return "" if module_namespace && !keep_when_namespaced
22
+ return register_only_block(indent, lazy) if module_namespace
23
+
24
+ full_block(class_name, indent, lazy)
25
+ end
26
+
27
+ def execution_line(class_name:, module_namespace:)
28
+ return "" if module_namespace
29
+
30
+ "\n#{class_name}.register_class_with_id\n"
31
+ end
32
+
33
+ def register_only_block(indent, lazy)
34
+ <<~REG.gsub(/^/, indent)
35
+
36
+ def self.register
37
+ #{indent}#{register_body(lazy)}
38
+ end
39
+ REG
40
+ end
41
+
42
+ def full_block(class_name, indent, lazy)
43
+ target = Utils.snake_case(class_name).to_sym
44
+ <<~REG.gsub(/^/, indent)
45
+
46
+ def self.register
47
+ #{indent}#{register_body(lazy)}
48
+ end
49
+
50
+ def self.register_class_with_id
51
+ #{indent}context = Lutaml::Model::GlobalContext.context(Lutaml::Model::Config.default_register)
52
+ #{indent}context.registry.register(:#{target}, self)
53
+ end
54
+ REG
55
+ end
56
+
57
+ def register_body(lazy)
58
+ lazy ? "@register ||= Lutaml::Model::Config.default_register" : "Lutaml::Model::Config.default_register"
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end