lutaml-model 0.7.3 → 0.7.5

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 (184) hide show
  1. checksums.yaml +4 -4
  2. data/.envrc +1 -0
  3. data/.github/workflows/release.yml +3 -0
  4. data/.gitignore +6 -1
  5. data/.irbrc +1 -0
  6. data/.pryrc +1 -0
  7. data/.rubocop_todo.yml +25 -52
  8. data/README.adoc +2177 -192
  9. data/docs/custom_registers.adoc +228 -0
  10. data/docs/schema_generation.adoc +898 -0
  11. data/docs/schema_import.adoc +364 -0
  12. data/flake.lock +114 -0
  13. data/flake.nix +103 -0
  14. data/lib/lutaml/model/attribute.rb +230 -94
  15. data/lib/lutaml/model/choice.rb +30 -0
  16. data/lib/lutaml/model/collection.rb +194 -0
  17. data/lib/lutaml/model/comparable_model.rb +3 -3
  18. data/lib/lutaml/model/config.rb +26 -3
  19. data/lib/lutaml/model/constants.rb +2 -0
  20. data/lib/lutaml/model/error/element_count_out_of_range_error.rb +29 -0
  21. data/lib/lutaml/model/error/invalid_attribute_name_error.rb +15 -0
  22. data/lib/lutaml/model/error/invalid_attribute_options_error.rb +16 -0
  23. data/lib/lutaml/model/error/invalid_choice_range_error.rb +3 -5
  24. data/lib/lutaml/model/error/register/not_registrable_class_error.rb +11 -0
  25. data/lib/lutaml/model/error/type/invalid_value_error.rb +5 -3
  26. data/lib/lutaml/model/error/type/max_bound_error.rb +20 -0
  27. data/lib/lutaml/model/error/type/max_length_error.rb +20 -0
  28. data/lib/lutaml/model/error/type/min_bound_error.rb +20 -0
  29. data/lib/lutaml/model/error/type/min_length_error.rb +20 -0
  30. data/lib/lutaml/model/error/type/pattern_not_matched_error.rb +18 -0
  31. data/lib/lutaml/model/error/validation_failed_error.rb +9 -0
  32. data/lib/lutaml/model/error.rb +10 -0
  33. data/lib/lutaml/model/errors.rb +36 -0
  34. data/lib/lutaml/model/format_registry.rb +5 -2
  35. data/lib/lutaml/model/global_register.rb +41 -0
  36. data/lib/lutaml/model/{hash.rb → hash_adapter.rb} +5 -5
  37. data/lib/lutaml/model/jsonl/document.rb +14 -0
  38. data/lib/lutaml/model/jsonl/mapping.rb +19 -0
  39. data/lib/lutaml/model/jsonl/mapping_rule.rb +9 -0
  40. data/lib/lutaml/model/jsonl/standard_adapter.rb +33 -0
  41. data/lib/lutaml/model/jsonl/transform.rb +19 -0
  42. data/lib/lutaml/model/jsonl.rb +21 -0
  43. data/lib/lutaml/model/key_value_document.rb +3 -2
  44. data/lib/lutaml/model/mapping/key_value_mapping.rb +64 -4
  45. data/lib/lutaml/model/mapping/key_value_mapping_rule.rb +4 -0
  46. data/lib/lutaml/model/mapping/mapping_rule.rb +8 -3
  47. data/lib/lutaml/model/register.rb +105 -0
  48. data/lib/lutaml/model/registrable.rb +6 -0
  49. data/lib/lutaml/model/schema/base_schema.rb +64 -0
  50. data/lib/lutaml/model/schema/decorators/attribute.rb +114 -0
  51. data/lib/lutaml/model/schema/decorators/choices.rb +31 -0
  52. data/lib/lutaml/model/schema/decorators/class_definition.rb +85 -0
  53. data/lib/lutaml/model/schema/decorators/definition_collection.rb +97 -0
  54. data/lib/lutaml/model/schema/generator/definition.rb +53 -0
  55. data/lib/lutaml/model/schema/generator/definitions_collection.rb +81 -0
  56. data/lib/lutaml/model/schema/generator/properties_collection.rb +63 -0
  57. data/lib/lutaml/model/schema/generator/property.rb +110 -0
  58. data/lib/lutaml/model/schema/generator/ref.rb +24 -0
  59. data/lib/lutaml/model/schema/helpers/template_helper.rb +49 -0
  60. data/lib/lutaml/model/schema/json_schema.rb +42 -49
  61. data/lib/lutaml/model/schema/relaxng_schema.rb +14 -10
  62. data/lib/lutaml/model/schema/renderer.rb +36 -0
  63. data/lib/lutaml/model/schema/shared_methods.rb +24 -0
  64. data/lib/lutaml/model/schema/templates/model.erb +9 -0
  65. data/lib/lutaml/model/schema/xml_compiler/attribute.rb +85 -0
  66. data/lib/lutaml/model/schema/xml_compiler/attribute_group.rb +45 -0
  67. data/lib/lutaml/model/schema/xml_compiler/choice.rb +65 -0
  68. data/lib/lutaml/model/schema/xml_compiler/complex_content.rb +27 -0
  69. data/lib/lutaml/model/schema/xml_compiler/complex_content_restriction.rb +34 -0
  70. data/lib/lutaml/model/schema/xml_compiler/complex_type.rb +136 -0
  71. data/lib/lutaml/model/schema/xml_compiler/element.rb +104 -0
  72. data/lib/lutaml/model/schema/xml_compiler/group.rb +97 -0
  73. data/lib/lutaml/model/schema/xml_compiler/restriction.rb +101 -0
  74. data/lib/lutaml/model/schema/xml_compiler/sequence.rb +50 -0
  75. data/lib/lutaml/model/schema/xml_compiler/simple_content.rb +36 -0
  76. data/lib/lutaml/model/schema/xml_compiler/simple_type.rb +189 -0
  77. data/lib/lutaml/model/schema/xml_compiler.rb +231 -587
  78. data/lib/lutaml/model/schema/xsd_schema.rb +12 -8
  79. data/lib/lutaml/model/schema/yaml_schema.rb +41 -35
  80. data/lib/lutaml/model/schema.rb +1 -0
  81. data/lib/lutaml/model/sequence.rb +60 -30
  82. data/lib/lutaml/model/serialize.rb +175 -53
  83. data/lib/lutaml/model/services/base.rb +11 -0
  84. data/lib/lutaml/model/services/logger.rb +2 -2
  85. data/lib/lutaml/model/services/rule_value_extractor.rb +92 -0
  86. data/lib/lutaml/model/services/type/validator/number.rb +25 -0
  87. data/lib/lutaml/model/services/type/validator/string.rb +52 -0
  88. data/lib/lutaml/model/services/type/validator.rb +43 -0
  89. data/lib/lutaml/model/services/validator.rb +145 -0
  90. data/lib/lutaml/model/services.rb +3 -0
  91. data/lib/lutaml/model/transform/key_value_transform.rb +60 -50
  92. data/lib/lutaml/model/transform/xml_transform.rb +46 -57
  93. data/lib/lutaml/model/transform.rb +22 -8
  94. data/lib/lutaml/model/type/boolean.rb +1 -1
  95. data/lib/lutaml/model/type/date.rb +1 -1
  96. data/lib/lutaml/model/type/date_time.rb +1 -1
  97. data/lib/lutaml/model/type/decimal.rb +11 -9
  98. data/lib/lutaml/model/type/float.rb +2 -1
  99. data/lib/lutaml/model/type/integer.rb +24 -21
  100. data/lib/lutaml/model/type/string.rb +4 -2
  101. data/lib/lutaml/model/type/time.rb +1 -1
  102. data/lib/lutaml/model/type/time_without_date.rb +1 -1
  103. data/lib/lutaml/model/type/value.rb +5 -1
  104. data/lib/lutaml/model/type.rb +5 -2
  105. data/lib/lutaml/model/utils.rb +30 -8
  106. data/lib/lutaml/model/validation.rb +6 -4
  107. data/lib/lutaml/model/version.rb +1 -1
  108. data/lib/lutaml/model/xml/document.rb +37 -19
  109. data/lib/lutaml/model/xml/mapping.rb +74 -13
  110. data/lib/lutaml/model/xml/mapping_rule.rb +10 -2
  111. data/lib/lutaml/model/xml/nokogiri_adapter.rb +5 -3
  112. data/lib/lutaml/model/xml/oga/element.rb +4 -1
  113. data/lib/lutaml/model/xml/oga_adapter.rb +4 -3
  114. data/lib/lutaml/model/xml/ox_adapter.rb +20 -6
  115. data/lib/lutaml/model/xml/xml_element.rb +3 -28
  116. data/lib/lutaml/model/xml_adapter/element.rb +1 -1
  117. data/lib/lutaml/model/xml_adapter/nokogiri_adapter.rb +1 -1
  118. data/lib/lutaml/model/xml_adapter/oga_adapter.rb +1 -1
  119. data/lib/lutaml/model/xml_adapter/ox_adapter.rb +1 -1
  120. data/lib/lutaml/model/yamls/document.rb +14 -0
  121. data/lib/lutaml/model/yamls/mapping.rb +19 -0
  122. data/lib/lutaml/model/yamls/mapping_rule.rb +9 -0
  123. data/lib/lutaml/model/yamls/standard_adapter.rb +34 -0
  124. data/lib/lutaml/model/yamls/transform.rb +19 -0
  125. data/lib/lutaml/model/yamls.rb +21 -0
  126. data/lib/lutaml/model.rb +7 -31
  127. data/spec/benchmarks/xml_parsing_benchmark_spec.rb +4 -5
  128. data/spec/fixtures/xml/advanced_test_schema.xsd +134 -0
  129. data/spec/fixtures/xml/examples/nested_categories.xml +55 -0
  130. data/spec/fixtures/xml/examples/valid_catalog.xml +43 -0
  131. data/spec/fixtures/xml/product_catalog.xsd +151 -0
  132. data/spec/fixtures/xml/specifications_schema.xsd +38 -0
  133. data/spec/lutaml/model/attribute_collection_spec.rb +101 -0
  134. data/spec/lutaml/model/attribute_spec.rb +41 -44
  135. data/spec/lutaml/model/choice_spec.rb +44 -0
  136. data/spec/lutaml/model/custom_collection_spec.rb +830 -0
  137. data/spec/lutaml/model/custom_model_spec.rb +15 -3
  138. data/spec/lutaml/model/defaults_spec.rb +5 -1
  139. data/spec/lutaml/model/global_register_spec.rb +108 -0
  140. data/spec/lutaml/model/group_spec.rb +9 -3
  141. data/spec/lutaml/model/jsonl/standard_adapter_spec.rb +91 -0
  142. data/spec/lutaml/model/jsonl_spec.rb +229 -0
  143. data/spec/lutaml/model/multiple_mapping_spec.rb +1 -1
  144. data/spec/lutaml/model/register/key_value_spec.rb +275 -0
  145. data/spec/lutaml/model/register/xml_spec.rb +187 -0
  146. data/spec/lutaml/model/register_spec.rb +147 -0
  147. data/spec/lutaml/model/rule_value_extractor_spec.rb +162 -0
  148. data/spec/lutaml/model/schema/generator/definitions_collection_spec.rb +120 -0
  149. data/spec/lutaml/model/schema/json_schema_spec.rb +412 -51
  150. data/spec/lutaml/model/schema/json_schema_to_models_spec.rb +383 -0
  151. data/spec/lutaml/model/schema/xml_compiler/attribute_group_spec.rb +65 -0
  152. data/spec/lutaml/model/schema/xml_compiler/attribute_spec.rb +63 -0
  153. data/spec/lutaml/model/schema/xml_compiler/choice_spec.rb +71 -0
  154. data/spec/lutaml/model/schema/xml_compiler/complex_content_restriction_spec.rb +55 -0
  155. data/spec/lutaml/model/schema/xml_compiler/complex_content_spec.rb +37 -0
  156. data/spec/lutaml/model/schema/xml_compiler/complex_type_spec.rb +173 -0
  157. data/spec/lutaml/model/schema/xml_compiler/element_spec.rb +63 -0
  158. data/spec/lutaml/model/schema/xml_compiler/group_spec.rb +86 -0
  159. data/spec/lutaml/model/schema/xml_compiler/restriction_spec.rb +76 -0
  160. data/spec/lutaml/model/schema/xml_compiler/sequence_spec.rb +59 -0
  161. data/spec/lutaml/model/schema/xml_compiler/simple_content_spec.rb +55 -0
  162. data/spec/lutaml/model/schema/xml_compiler/simple_type_spec.rb +181 -0
  163. data/spec/lutaml/model/schema/xml_compiler_spec.rb +503 -1804
  164. data/spec/lutaml/model/schema/yaml_schema_spec.rb +249 -26
  165. data/spec/lutaml/model/sequence_spec.rb +36 -0
  166. data/spec/lutaml/model/serializable_spec.rb +31 -0
  167. data/spec/lutaml/model/type_spec.rb +8 -4
  168. data/spec/lutaml/model/utils_spec.rb +3 -3
  169. data/spec/lutaml/model/xml/derived_attributes_spec.rb +1 -1
  170. data/spec/lutaml/model/xml/xml_element_spec.rb +7 -1
  171. data/spec/lutaml/model/xml_adapter/xml_namespace_spec.rb +6 -6
  172. data/spec/lutaml/model/xml_adapter_spec.rb +24 -0
  173. data/spec/lutaml/model/xml_mapping_rule_spec.rb +11 -4
  174. data/spec/lutaml/model/xml_mapping_spec.rb +1 -1
  175. data/spec/lutaml/model/yamls/standard_adapter_spec.rb +183 -0
  176. data/spec/lutaml/model/yamls_spec.rb +294 -0
  177. data/spec/spec_helper.rb +1 -0
  178. metadata +105 -9
  179. data/lib/lutaml/model/schema/templates/simple_type.rb +0 -247
  180. /data/lib/lutaml/model/{hash → hash_adapter}/document.rb +0 -0
  181. /data/lib/lutaml/model/{hash → hash_adapter}/mapping.rb +0 -0
  182. /data/lib/lutaml/model/{hash → hash_adapter}/mapping_rule.rb +0 -0
  183. /data/lib/lutaml/model/{hash → hash_adapter}/standard_adapter.rb +0 -0
  184. /data/lib/lutaml/model/{hash → hash_adapter}/transform.rb +0 -0
@@ -0,0 +1,275 @@
1
+ require "spec_helper"
2
+
3
+ module RegisterKeyValueSpec
4
+ class GeoCoordinate < Lutaml::Model::Serializable
5
+ attribute :latitude, :float
6
+ attribute :longitude, :float
7
+
8
+ json do
9
+ map :lat, to: :latitude
10
+ map :lng, to: :longitude
11
+ end
12
+ end
13
+
14
+ class Address < Lutaml::Model::Serializable
15
+ attribute :street, :string
16
+ attribute :city, :string
17
+ attribute :location, GeoCoordinate
18
+
19
+ json do
20
+ map :street, to: :street
21
+ map :city, to: :city
22
+ map :geo, to: :location
23
+ end
24
+ end
25
+
26
+ class ContactInfo < Lutaml::Model::Serializable
27
+ attribute :phone, :string
28
+ attribute :email, :string
29
+
30
+ json do
31
+ map :phoneNumber, to: :phone
32
+ map :email, to: :email
33
+ end
34
+ end
35
+
36
+ class Person < Lutaml::Model::Serializable
37
+ attribute :name, :string
38
+ attribute :age, :integer
39
+ attribute :address, :address
40
+ attribute :contact, :contact_info
41
+ attribute :tags, :string, collection: true
42
+
43
+ json do
44
+ map :name, to: :name
45
+ map :age, to: :age
46
+ map :address, to: :address
47
+ map :contactInfo, to: :contact
48
+ map :tags, to: :tags
49
+ end
50
+ end
51
+
52
+ class EnhancedContactInfo < Lutaml::Model::Serializable
53
+ attribute :phone, :string
54
+ attribute :email, :string
55
+ attribute :preferred, :boolean
56
+
57
+ json do
58
+ map :phoneNumber, to: :phone
59
+ map :email, to: :email
60
+ map :isPrimary, to: :preferred
61
+ end
62
+ end
63
+ end
64
+
65
+ RSpec.describe "RegisterKeyValueSpec" do
66
+ let(:register) { Lutaml::Model::Register.new(:json_test_register) }
67
+ let(:person) { RegisterKeyValueSpec::Person.from_json(json, register: register) }
68
+
69
+ before do
70
+ # Register the registers in the global registry
71
+ Lutaml::Model::GlobalRegister.register(register)
72
+
73
+ # Register all the V1 model classes in the register
74
+ register.register_model_tree(RegisterKeyValueSpec::Person)
75
+ register.register_model(RegisterKeyValueSpec::Address)
76
+ register.register_model(RegisterKeyValueSpec::GeoCoordinate)
77
+ register.register_model(RegisterKeyValueSpec::ContactInfo)
78
+ end
79
+
80
+ describe "parsing JSON" do
81
+ let(:json) do
82
+ <<~JSON
83
+ {
84
+ "name": "John Doe",
85
+ "age": 30,
86
+ "address": {
87
+ "street": "123 Main St",
88
+ "city": "Anytown",
89
+ "geo": {
90
+ "lat": 40.7128,
91
+ "lng": -74.0060
92
+ }
93
+ },
94
+ "contactInfo": {
95
+ "phoneNumber": "555-1234",
96
+ "email": "john@example.com"
97
+ },
98
+ "tags": ["developer", "musician"]
99
+ }
100
+ JSON
101
+ end
102
+
103
+ it "parses JSON into model objects" do
104
+ expect(person).to be_a(RegisterKeyValueSpec::Person)
105
+ expect(person.name).to eq("John Doe")
106
+ expect(person.age).to eq(30)
107
+ expect(person.address).to be_a(RegisterKeyValueSpec::Address)
108
+ expect(person.address.street).to eq("123 Main St")
109
+ expect(person.address.city).to eq("Anytown")
110
+ expect(person.address.location).to be_a(RegisterKeyValueSpec::GeoCoordinate)
111
+ expect(person.address.location.latitude).to eq(40.7128)
112
+ expect(person.address.location.longitude).to eq(-74.0060)
113
+ expect(person.contact).to be_a(RegisterKeyValueSpec::ContactInfo)
114
+ expect(person.contact.phone).to eq("555-1234")
115
+ expect(person.contact.email).to eq("john@example.com")
116
+ expect(person.tags).to eq(["developer", "musician"])
117
+ end
118
+
119
+ it "serializes model objects back to JSON" do
120
+ json_output = person.to_json
121
+ parsed_json = JSON.parse(json_output)
122
+
123
+ expect(parsed_json["name"]).to eq("John Doe")
124
+ expect(parsed_json["age"]).to eq(30)
125
+ expect(parsed_json["address"]["street"]).to eq("123 Main St")
126
+ expect(parsed_json["address"]["city"]).to eq("Anytown")
127
+ expect(parsed_json["address"]["geo"]["lat"]).to eq(40.7128)
128
+ expect(parsed_json["address"]["geo"]["lng"]).to eq(-74.0060)
129
+ expect(parsed_json["contactInfo"]["phoneNumber"]).to eq("555-1234")
130
+ expect(parsed_json["contactInfo"]["email"]).to eq("john@example.com")
131
+ expect(parsed_json["tags"]).to eq(["developer", "musician"])
132
+ end
133
+ end
134
+
135
+ describe "using global type substitution with JSON" do
136
+ let(:register_substitution) do
137
+ register.register_global_type_substitution(
138
+ from_type: RegisterKeyValueSpec::ContactInfo,
139
+ to_type: RegisterKeyValueSpec::EnhancedContactInfo,
140
+ )
141
+ end
142
+
143
+ let(:json) do
144
+ <<~JSON
145
+ {
146
+ "name": "Jane Smith",
147
+ "age": 28,
148
+ "address": {
149
+ "street": "456 Oak Ave",
150
+ "city": "Somewhere",
151
+ "geo": {
152
+ "lat": 37.7749,
153
+ "lng": -122.4194
154
+ }
155
+ },
156
+ "contactInfo": {
157
+ "phoneNumber": "555-5678",
158
+ "email": "jane@example.com",
159
+ "isPrimary": true
160
+ },
161
+ "tags": ["designer"]
162
+ }
163
+ JSON
164
+ end
165
+
166
+ context "when the substitute class is not registered" do
167
+ it "deserializes contactInfo using ContactInfo class" do
168
+ expect(person.contact).to be_a(RegisterKeyValueSpec::ContactInfo)
169
+ expect(person.contact).not_to respond_to(:preferred)
170
+ expect(person.contact.phone).to eq("555-5678")
171
+ expect(person.contact.email).to eq("jane@example.com")
172
+ end
173
+ end
174
+
175
+ context "when the substitute class is registered" do
176
+ it "deserializes contactInfo using EnhancedContactInfo class" do
177
+ register.register_model(RegisterKeyValueSpec::EnhancedContactInfo)
178
+ register_substitution
179
+
180
+ enhanced_person = RegisterKeyValueSpec::Person.from_json(json, register: register.id)
181
+
182
+ expect(enhanced_person.contact).to be_a(RegisterKeyValueSpec::EnhancedContactInfo)
183
+ expect(enhanced_person.contact).to respond_to(:preferred)
184
+ expect(enhanced_person.contact.phone).to eq("555-5678")
185
+ expect(enhanced_person.contact.email).to eq("jane@example.com")
186
+ expect(enhanced_person.contact.preferred).to be(true)
187
+
188
+ # Ensure serialization includes the new field
189
+ json_output = enhanced_person.to_json
190
+ parsed_json = JSON.parse(json_output)
191
+ expect(parsed_json["contactInfo"]["isPrimary"]).to be(true)
192
+ end
193
+ end
194
+ end
195
+
196
+ describe "handling complex nested JSON arrays" do
197
+ let(:complex_json) do
198
+ <<~JSON
199
+ {
200
+ "name": "Team Alpha",
201
+ "members": [
202
+ {
203
+ "name": "Alice",
204
+ "age": 32,
205
+ "address": {
206
+ "street": "789 Pine St",
207
+ "city": "Elsewhere"
208
+ }
209
+ },
210
+ {
211
+ "name": "Bob",
212
+ "age": 29,
213
+ "address": {
214
+ "street": "321 Elm St",
215
+ "city": "Nowhere"
216
+ }
217
+ }
218
+ ]
219
+ }
220
+ JSON
221
+ end
222
+
223
+ before do
224
+ stub_const("RegisterKeyValueSpec::Team", Class.new(Lutaml::Model::Serializable) do
225
+ attribute :name, :string
226
+ attribute :members, :person, collection: true
227
+
228
+ json do
229
+ map :name, to: :name
230
+ map :members, to: :members
231
+ end
232
+ end)
233
+
234
+ register.register_model(RegisterKeyValueSpec::Team)
235
+ end
236
+
237
+ it "correctly handles arrays of complex objects" do
238
+ team = RegisterKeyValueSpec::Team.from_json(complex_json, register: register)
239
+
240
+ expect(team.name).to eq("Team Alpha")
241
+ expect(team.members.size).to eq(2)
242
+ expect(team.members[0].name).to eq("Alice")
243
+ expect(team.members[0].age).to eq(32)
244
+ expect(team.members[0].address.street).to eq("789 Pine St")
245
+ expect(team.members[1].name).to eq("Bob")
246
+ expect(team.members[1].age).to eq(29)
247
+ expect(team.members[1].address.street).to eq("321 Elm St")
248
+
249
+ # Test round-trip serialization
250
+ toml_output = team.to_toml
251
+ team2 = RegisterKeyValueSpec::Team.from_toml(toml_output, register: register)
252
+
253
+ expect(team2).to eq(team)
254
+ expect(team2.members[0].name).to eq("Alice")
255
+ expect(team2.members[1].name).to eq("Bob")
256
+ end
257
+ end
258
+
259
+ describe "#resolve" do
260
+ before do
261
+ stub_const("RegisterKeyValueSpec::Team", Class.new(Lutaml::Model::Serializable))
262
+ register.register_model(RegisterKeyValueSpec::Team)
263
+ end
264
+
265
+ it "resolves a class" do
266
+ expect(register.resolve("RegisterKeyValueSpec::Team")).to eq(RegisterKeyValueSpec::Team)
267
+ expect(register.resolve(:"RegisterKeyValueSpec::Team")).to eq(RegisterKeyValueSpec::Team)
268
+ expect(register.resolve(RegisterKeyValueSpec::Team)).to eq(RegisterKeyValueSpec::Team)
269
+ end
270
+
271
+ it "returns nil for an unknown class" do
272
+ expect(register.resolve("UnknownClass")).to be_nil
273
+ end
274
+ end
275
+ end
@@ -0,0 +1,187 @@
1
+ require "spec_helper"
2
+
3
+ module RegisterXmlSpec
4
+ class String < Lutaml::Model::Type::String
5
+ def to_xml
6
+ "custom_string: #{value}"
7
+ end
8
+ end
9
+
10
+ class Mi < Lutaml::Model::Serializable
11
+ attribute :value, :string
12
+
13
+ xml do
14
+ root "mi"
15
+ namespace "http://www.w3.org/1998/Math/MathML"
16
+
17
+ map_content to: :value
18
+ end
19
+ end
20
+
21
+ # Define a MathML Operator element class
22
+ class Mo < Lutaml::Model::Serializable
23
+ attribute :value, :string
24
+
25
+ xml do
26
+ root "mo"
27
+ namespace "http://www.w3.org/1998/Math/MathML"
28
+
29
+ map_content to: :value
30
+ end
31
+ end
32
+
33
+ # Define a MathML Fraction element
34
+ class Mfrac < Lutaml::Model::Serializable
35
+ attribute :numerator, :mo
36
+ attribute :denominator, :mi
37
+
38
+ xml do
39
+ root "mfrac"
40
+ namespace "http://www.w3.org/1998/Math/MathML"
41
+
42
+ map_element "mo", to: :numerator
43
+ map_element "mi", to: :denominator
44
+ end
45
+ end
46
+
47
+ # Define a full MathML expression
48
+ class Math < Lutaml::Model::Serializable
49
+ attribute :symbol, :mi
50
+ attribute :operator, :mo
51
+ attribute :fraction, :mfrac
52
+
53
+ xml do
54
+ root "math"
55
+ namespace "http://www.w3.org/1998/Math/MathML"
56
+
57
+ map_element "mi", to: :symbol
58
+ map_element "mo", to: :operator
59
+ map_element "mfrac", to: :fraction
60
+ end
61
+ end
62
+
63
+ class NewMi < Lutaml::Model::Serializable
64
+ attribute :value, :string
65
+ attribute :color, :string
66
+
67
+ xml do
68
+ root "mi"
69
+ namespace "http://www.w3.org/1998/Math/MathML"
70
+
71
+ map_content to: :value
72
+ map_attribute :color, to: :color
73
+ end
74
+ end
75
+ end
76
+
77
+ RSpec.describe "RegisterXmlSpec" do
78
+ let(:register) { Lutaml::Model::Register.new(:mathml_register) }
79
+ let(:formula) { RegisterXmlSpec::Math.from_xml(xml, register: register) }
80
+
81
+ before do
82
+ # Register the register in the global registry
83
+ Lutaml::Model::GlobalRegister.register(register)
84
+
85
+ # Register all the model classes
86
+ register.register_model_tree(RegisterXmlSpec::Math)
87
+ register.register_model(RegisterXmlSpec::Mi)
88
+ register.register_model(RegisterXmlSpec::Mo)
89
+ register.register_model(RegisterXmlSpec::Mfrac)
90
+ end
91
+
92
+ describe "parsing MathML XML" do
93
+ let(:xml) do
94
+ <<~XML
95
+ <math xmlns="http://www.w3.org/1998/Math/MathML">
96
+ <mi>x</mi>
97
+ <mo>=</mo>
98
+ <mfrac>
99
+ <mo>a</mo>
100
+ <mi>b</mi>
101
+ </mfrac>
102
+ </math>
103
+ XML
104
+ end
105
+
106
+ let(:instantiated) do
107
+ register.get_class(:math).new(
108
+ symbol: RegisterXmlSpec::Mi.new(value: "x"),
109
+ operator: RegisterXmlSpec::Mo.new(value: "="),
110
+ fraction: RegisterXmlSpec::Mfrac.new(
111
+ {
112
+ numerator: RegisterXmlSpec::Mo.new(value: "a"),
113
+ denominator: RegisterXmlSpec::Mi.new(value: "b"),
114
+ },
115
+ {
116
+ register: register,
117
+ },
118
+ ),
119
+ )
120
+ end
121
+
122
+ it "parses MathML XML into model objects" do
123
+ expect(formula).to be_a(RegisterXmlSpec::Math)
124
+ expect(formula.symbol.value).to eq("x")
125
+ expect(formula.operator.value).to eq("=")
126
+ expect(formula.fraction.numerator.value).to eq("a")
127
+ expect(formula.fraction.denominator.value).to eq("b")
128
+ end
129
+
130
+ it "serializes model objects back to MathML XML" do
131
+ expect(formula.to_xml).to be_equivalent_to(xml)
132
+ end
133
+
134
+ it "instantiates the model correctly" do
135
+ expect(instantiated.to_xml).to be_equivalent_to(xml)
136
+ end
137
+ end
138
+
139
+ describe "using global type substitution with MathML" do
140
+ let(:register_substitution) do
141
+ register.register_global_type_substitution(
142
+ from_type: RegisterXmlSpec::Mi,
143
+ to_type: RegisterXmlSpec::NewMi,
144
+ )
145
+ register.register_global_type_substitution(
146
+ from_type: Lutaml::Model::Type::String,
147
+ to_type: RegisterXmlSpec::String,
148
+ )
149
+ end
150
+
151
+ let(:xml) do
152
+ <<~XML
153
+ <math xmlns="http://www.w3.org/1998/Math/MathML">
154
+ <mi color="red">y</mi>
155
+ </math>
156
+ XML
157
+ end
158
+
159
+ let(:xml_type_substituted) do
160
+ <<~XML
161
+ <math xmlns="http://www.w3.org/1998/Math/MathML">
162
+ <mi color="custom_string: red">y</mi>
163
+ </math>
164
+ XML
165
+ end
166
+
167
+ context "when substitute class is not registered" do
168
+ it "serializes mi tag using Mi class" do
169
+ expect(formula.symbol).to be_a(RegisterXmlSpec::Mi)
170
+ expect(formula.symbol).not_to respond_to(:color)
171
+ expect(formula.symbol.value).to eq("y")
172
+ expect(formula.to_xml).not_to be_equivalent_to(xml)
173
+ end
174
+ end
175
+
176
+ context "when substitute class is registered" do
177
+ it "serializes mi tag using NewMi class" do
178
+ register_substitution
179
+ expect(formula.symbol).to be_a(RegisterXmlSpec::NewMi)
180
+ expect(formula.symbol).to respond_to(:color)
181
+ expect(formula.symbol.color).to eq("red")
182
+ expect(formula.symbol.value).to eq("y")
183
+ expect(formula.to_xml).to be_equivalent_to(xml_type_substituted)
184
+ end
185
+ end
186
+ end
187
+ end
@@ -0,0 +1,147 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+ module RegisterSpec
5
+ class CustomString < Lutaml::Model::Type::String; end
6
+ class CustomInteger < Lutaml::Model::Type::Integer; end
7
+
8
+ Lutaml::Model::Type.register(:custom_string, CustomString)
9
+
10
+ class Address < Lutaml::Model::Serializable
11
+ attribute :location, :string
12
+ attribute :postal_code, :custom_string
13
+ end
14
+ end
15
+
16
+ RSpec.describe Lutaml::Model::Register do
17
+ describe "#initialize" do
18
+ it "initializes with id" do
19
+ register = described_class.new(:v1)
20
+ expect(register.id).to eq(:v1)
21
+ expect(register.models).to eq({})
22
+ end
23
+ end
24
+
25
+ describe "#register_model" do
26
+ let(:v1_register) { described_class.new(:v1) }
27
+
28
+ before do
29
+ v1_register.register_model(RegisterSpec::CustomString, id: :custom_string)
30
+ v1_register.register_model(RegisterSpec::CustomInteger, id: :custom_integer)
31
+ end
32
+
33
+ it "registers model with explicit id" do
34
+ expect(v1_register.models[:custom_string]).to be_nil
35
+ end
36
+
37
+ it "allows overriding an existing type" do
38
+ v1_register.register_model(Lutaml::Model::Type::String, id: :custom_string)
39
+ expect(v1_register.models[:custom_string]).to be_nil
40
+ end
41
+
42
+ it "registers serializable class" do
43
+ v1_register.register_model(RegisterSpec::Address, id: :address)
44
+ expect(v1_register.models[:address]).to eq(RegisterSpec::Address)
45
+ end
46
+
47
+ it "registers model without explicit id" do
48
+ stub_const("TestModel", Class.new(Lutaml::Model::Serializable))
49
+ v1_register.register_model(TestModel)
50
+ expect(v1_register.models[:test_model]).to eq(TestModel)
51
+ end
52
+ end
53
+
54
+ describe "#resolve" do
55
+ let(:v1_register) { described_class.new(:v1) }
56
+
57
+ before do
58
+ v1_register.register_model(RegisterSpec::Address, id: :address)
59
+ end
60
+
61
+ it "finds registered class by string representation" do
62
+ expect(v1_register.resolve("RegisterSpec::Address")).to eq(RegisterSpec::Address)
63
+ end
64
+
65
+ it "returns nil for unregistered class" do
66
+ expect(v1_register.resolve("UnknownClass")).to be_nil
67
+ end
68
+ end
69
+
70
+ describe "#get_class" do
71
+ let(:v1_register) { described_class.new(:v1) }
72
+
73
+ before do
74
+ v1_register.register_model(Lutaml::Model::Type::String, id: :custom_type)
75
+ end
76
+
77
+ it "returns registered class by key" do
78
+ expect(v1_register.get_class(:custom_type)).to eq(Lutaml::Model::Type::String)
79
+ end
80
+
81
+ it "returns class by string using constant lookup" do
82
+ expect(v1_register.get_class("String")).to eq(Lutaml::Model::Type::String)
83
+ end
84
+
85
+ it "returns class by symbol using Type.lookup" do
86
+ allow(Lutaml::Model::Type).to receive(:lookup).with(:String).and_return(Lutaml::Model::Type::String)
87
+ expect(v1_register.get_class(:String)).to eq(Lutaml::Model::Type::String)
88
+ end
89
+
90
+ it "returns class directly if class is provided" do
91
+ expect(v1_register.get_class(Lutaml::Model::Type::String)).to eq(Lutaml::Model::Type::String)
92
+ end
93
+
94
+ it "raises error for unsupported type" do
95
+ expect { v1_register.get_class(123) }.to raise_error(Lutaml::Model::UnknownTypeError)
96
+ end
97
+ end
98
+
99
+ describe "#register_model_tree" do
100
+ let(:v1_register) { described_class.new(:v1) }
101
+
102
+ context "when registering a valid model" do
103
+ let(:model_class) do
104
+ Class.new(Lutaml::Model::Serializable) do
105
+ attribute :nested_address, RegisterSpec::Address
106
+ end
107
+ end
108
+
109
+ it "registers the model and its nested attributes" do
110
+ v1_register.register_model_tree(model_class)
111
+ expect(v1_register.models.values).to include(model_class)
112
+ expect(v1_register.models.values).to include(RegisterSpec::Address)
113
+ end
114
+ end
115
+ end
116
+
117
+ describe "#register_global_type_substitution" do
118
+ let(:v1_register) { described_class.new(:v1) }
119
+
120
+ it "registers a global type substitution" do
121
+ v1_register.register_global_type_substitution(from_type: :string, to_type: :text)
122
+ expect(v1_register.instance_variable_get(:@global_substitutions)).to include(string: :text)
123
+ end
124
+ end
125
+
126
+ describe "#register_attributes" do
127
+ let(:v1_register) { described_class.new(:v1) }
128
+ let(:model_class) do
129
+ Class.new(Lutaml::Model::Serializable) do
130
+ attribute :nested_address, RegisterSpec::Address
131
+ attribute :string_attr, :string
132
+ end
133
+ end
134
+
135
+ it "registers non-builtin type attributes" do
136
+ attributes = model_class.attributes
137
+ v1_register.register_attributes(attributes)
138
+ expect(v1_register.models.values).to include(RegisterSpec::Address)
139
+ end
140
+
141
+ it "doesn't register built-in types" do
142
+ attributes = model_class.attributes
143
+ v1_register.register_attributes(attributes)
144
+ expect(v1_register.models.keys).not_to include(:string)
145
+ end
146
+ end
147
+ end