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
@@ -1,11 +1,19 @@
1
1
  require "spec_helper"
2
2
 
3
3
  class CustomModelChild
4
- attr_accessor :street, :city
4
+ attr_accessor :street, :city, :register
5
+
6
+ def initialize(register = nil)
7
+ @register = register
8
+ end
5
9
  end
6
10
 
7
11
  class CustomModelParent
8
- attr_accessor :first_name, :middle_name, :last_name, :child_mapper, :math
12
+ attr_accessor :first_name, :middle_name, :last_name, :child_mapper, :math, :register
13
+
14
+ def initialize(register = nil)
15
+ @register = register
16
+ end
9
17
 
10
18
  def name
11
19
  "#{first_name} #{last_name}"
@@ -13,7 +21,11 @@ class CustomModelParent
13
21
  end
14
22
 
15
23
  class GenericFormulaClass
16
- attr_accessor :value
24
+ attr_accessor :value, :register
25
+
26
+ def initialize(register = nil)
27
+ @register = register
28
+ end
17
29
  end
18
30
 
19
31
  class Mi < Lutaml::Model::Serializable
@@ -108,7 +108,11 @@ module DefaultsSpec
108
108
 
109
109
  # Class for testing render_default: true with custom model
110
110
  class Lang
111
- attr_accessor :lang, :content
111
+ attr_accessor :lang, :content, :register
112
+
113
+ def initialize(register = nil)
114
+ @register = register
115
+ end
112
116
  end
113
117
 
114
118
  class CustomModelWithDefaultValue < Lutaml::Model::Serializable
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe Lutaml::Model::GlobalRegister do
6
+ describe "#initialize" do
7
+ it "initializes with a default register" do
8
+ expect(described_class.instance.instance_variable_get(:@registers)).to include(default: described_class.lookup(:default))
9
+ end
10
+ end
11
+
12
+ describe "#register" do
13
+ it "adds register to the internal hash" do
14
+ register = Lutaml::Model::Register.new(:test_register)
15
+ described_class.register(register)
16
+
17
+ registers = described_class.instance.instance_variable_get(:@registers)
18
+ expect(registers[:test_register]).to eq(register)
19
+ end
20
+ end
21
+
22
+ describe ".register" do
23
+ it "calls instance method to register" do
24
+ register = Lutaml::Model::Register.new(:temp_register)
25
+ expect { described_class.register(register) }.to(
26
+ change { described_class.instance.instance_variable_get(:@registers).count }.by(1),
27
+ )
28
+ end
29
+ end
30
+
31
+ describe "#lookup" do
32
+ let(:register_v_one) { Lutaml::Model::Register.new(:v1) }
33
+ let(:register_v_two) { Lutaml::Model::Register.new(:v2) }
34
+
35
+ before do
36
+ described_class.instance.register(register_v_one)
37
+ described_class.instance.register(register_v_two)
38
+ end
39
+
40
+ it "returns the correct register for a given id" do
41
+ expect(described_class.instance.lookup(:v1)).to eq(register_v_one)
42
+ expect(described_class.instance.lookup(:v2)).to eq(register_v_two)
43
+ end
44
+
45
+ it "returns nil for non-existent id" do
46
+ expect(described_class.instance.lookup(:non_existent)).to be_nil
47
+ end
48
+
49
+ it "converts string id to symbol" do
50
+ expect(described_class.instance.lookup("v1")).to eq(register_v_one)
51
+ end
52
+ end
53
+
54
+ describe ".lookup" do
55
+ let(:register_v_one) { Lutaml::Model::Register.new(:v1) }
56
+
57
+ it "calls instance method to lookup" do
58
+ allow(described_class.instance).to receive(:lookup).with(:v1).and_return(register_v_one)
59
+
60
+ expect(described_class.lookup(:v1)).to eq(register_v_one)
61
+ end
62
+ end
63
+
64
+ describe "singleton behavior" do
65
+ it "returns the same instance on multiple calls" do
66
+ instance1 = described_class.instance
67
+ instance2 = described_class.instance
68
+
69
+ expect(instance1).to be(instance2)
70
+ end
71
+
72
+ it "shares state between method calls" do
73
+ register = Lutaml::Model::Register.new(:shared_test)
74
+ described_class.register(register)
75
+ expect(described_class.lookup(:shared_test)).to eq(register)
76
+ end
77
+ end
78
+
79
+ describe "#remove" do
80
+ let(:register_v_one) { Lutaml::Model::Register.new(:v1) }
81
+ let(:register_v_two) { Lutaml::Model::Register.new(:v2) }
82
+
83
+ before do
84
+ described_class.register(register_v_one)
85
+ described_class.register(register_v_two)
86
+ end
87
+
88
+ it "removes the specified register" do
89
+ register = described_class.instance
90
+ expect(register.instance_variable_get(:@registers).values).to include(register_v_one)
91
+ expect { described_class.remove(:v1) }.to(
92
+ change { register.instance_variable_get(:@registers).values.count }.by(-1),
93
+ )
94
+ expect(register.instance_variable_get(:@registers).values).not_to include(register_v_one)
95
+ end
96
+
97
+ it "does not remove other registers" do
98
+ described_class.remove(:v1)
99
+ registers = described_class.instance.instance_variable_get(:@registers)
100
+ expect(registers.values).to include(register_v_two)
101
+ end
102
+
103
+ it "does nothing when the specified register does not exist" do
104
+ registers = described_class.instance.instance_variable_get(:@registers)
105
+ expect { described_class.remove(:non_existent) }.not_to(change { registers.values.count })
106
+ end
107
+ end
108
+ end
@@ -60,12 +60,12 @@ module GroupSpec
60
60
  map_attribute "tag", to: :tag
61
61
  map_content to: :content
62
62
  map_element :group, to: :group
63
- import_model_mappings GroupOfItems
63
+ import_model_mappings :group_of_items
64
64
  end
65
65
  end
66
66
 
67
67
  class SimpleType < Lutaml::Model::Serializable
68
- import_model GroupOfItems
68
+ import_model :group_of_items
69
69
  end
70
70
 
71
71
  class GenericType < Lutaml::Model::Serializable
@@ -262,6 +262,12 @@ RSpec.describe "Group" do
262
262
  end
263
263
 
264
264
  context "with model" do
265
+ before do
266
+ Lutaml::Model::GlobalRegister
267
+ .lookup(Lutaml::Model::Config.default_register)
268
+ .register_model(GroupSpec::GroupOfItems, id: :group_of_items)
269
+ end
270
+
265
271
  shared_examples "imports attributes from" do |source_class, target_class|
266
272
  it "#{source_class.name} correctly" do
267
273
  source_attributes = source_class.attributes
@@ -339,7 +345,7 @@ RSpec.describe "Group" do
339
345
  context "when update the imported attribute" do
340
346
  it "updates the attribute `mstyle` only in `Mrow`" do
341
347
  GroupSpec::Mrow.attributes[:mstyle].instance_variable_set(:@type, :integer)
342
- expect(GroupSpec::Mrow.attributes[:mstyle].type).to eq(:integer)
348
+ expect(GroupSpec::Mrow.attributes[:mstyle].type).to eq(Lutaml::Model::Type::Integer)
343
349
  end
344
350
 
345
351
  it "maintains original type for the attribute `mstyle` in `Mfrac`" do
@@ -0,0 +1,91 @@
1
+ require "spec_helper"
2
+
3
+ RSpec.describe Lutaml::Model::Jsonl::StandardAdapter do
4
+ let(:valid_jsonl_content) do
5
+ <<~JSONL
6
+ {"name": "John", "age": 30}
7
+ {"name": "Jane", "age": 25}
8
+ {"name": "Bob", "age": 35}
9
+ JSONL
10
+ end
11
+
12
+ let(:invalid_jsonl_content) do
13
+ <<~JSONL
14
+ {"name": "John", "age": 30}
15
+ invalid json
16
+ {"name": "Bob", "age": 35}
17
+ JSONL
18
+ end
19
+
20
+ describe ".parse" do
21
+ context "with valid JSONL content" do
22
+ it "parses each line as a JSON object" do
23
+ results = described_class.parse(valid_jsonl_content)
24
+ expect(results).to be_an(Array)
25
+ expect(results.length).to eq(3)
26
+ expect(results.first).to eq({ "name" => "John", "age" => 30 })
27
+ expect(results.last).to eq({ "name" => "Bob", "age" => 35 })
28
+ end
29
+ end
30
+
31
+ context "with invalid JSONL content" do
32
+ it "skips invalid lines and continues parsing" do
33
+ results = described_class.parse(invalid_jsonl_content)
34
+ expect(results).to be_an(Array)
35
+ expect(results.length).to eq(2)
36
+ expect(results.first).to eq({ "name" => "John", "age" => 30 })
37
+ expect(results.last).to eq({ "name" => "Bob", "age" => 35 })
38
+ end
39
+ end
40
+
41
+ context "with empty lines" do
42
+ let(:jsonl_with_empty_lines) do
43
+ <<~JSONL
44
+ {"name": "John"}
45
+
46
+ {"name": "Jane"}
47
+ JSONL
48
+ end
49
+
50
+ it "skips empty lines" do
51
+ results = described_class.parse(jsonl_with_empty_lines)
52
+ expect(results).to be_an(Array)
53
+ expect(results.length).to eq(2)
54
+ end
55
+ end
56
+ end
57
+
58
+ describe "#to_jsonl" do
59
+ let(:adapter) { described_class.new([{ "name" => "John", "age" => 30 }]) }
60
+
61
+ it "generates JSONL format" do
62
+ expect(adapter.to_jsonl).to eq('{"name":"John","age":30}')
63
+ end
64
+
65
+ context "with multiple objects" do
66
+ let(:adapter) do
67
+ described_class.new([
68
+ { "name" => "John", "age" => 30 },
69
+ { "name" => "Jane", "age" => 25 },
70
+ ])
71
+ end
72
+
73
+ let(:expected_jsonl) do
74
+ <<~JSONL.strip
75
+ {"name":"John","age":30}
76
+ {"name":"Jane","age":25}
77
+ JSONL
78
+ end
79
+
80
+ it "generates multiple lines" do
81
+ expect(adapter.to_jsonl).to eq(expected_jsonl)
82
+ end
83
+ end
84
+ end
85
+
86
+ describe "FORMAT_SYMBOL" do
87
+ it "returns :jsonl" do
88
+ expect(described_class::FORMAT_SYMBOL).to eq(:jsonl)
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,229 @@
1
+ require "spec_helper"
2
+
3
+ module JsonlSpec
4
+ class Address < Lutaml::Model::Serializable
5
+ attribute :city, :string
6
+
7
+ json do
8
+ map "city", to: :city
9
+ end
10
+ end
11
+
12
+ class Person < Lutaml::Model::Serializable
13
+ attribute :name, :string
14
+ attribute :age, :integer
15
+ attribute :address, Address
16
+
17
+ json do
18
+ map "full_name", to: :name
19
+ map "age", to: :age
20
+ map "address", to: :address
21
+ end
22
+ end
23
+
24
+ class Directory < Lutaml::Model::Collection
25
+ instances :persons, Person
26
+
27
+ jsonl do
28
+ map_instances to: :persons
29
+ end
30
+ end
31
+ end
32
+
33
+ RSpec.describe "Jsonl" do
34
+ let(:john) do
35
+ JsonlSpec::Person.new(
36
+ {
37
+ name: "John",
38
+ age: "30",
39
+ address: JsonlSpec::Address.new({ city: "New York" }),
40
+ },
41
+ )
42
+ end
43
+
44
+ let(:jane) do
45
+ JsonlSpec::Person.new(
46
+ {
47
+ name: "Jane",
48
+ age: "25",
49
+ address: JsonlSpec::Address.new({ city: "London" }),
50
+ },
51
+ )
52
+ end
53
+
54
+ let(:bob) do
55
+ JsonlSpec::Person.new(
56
+ {
57
+ name: "Bob",
58
+ age: "35",
59
+ address: JsonlSpec::Address.new({ city: "Paris" }),
60
+ },
61
+ )
62
+ end
63
+
64
+ let(:valid_jsonl) do
65
+ <<~JSONL.strip
66
+ {"full_name":"John","age":30,"address":{"city":"New York"}}
67
+ {"full_name":"Jane","age":25,"address":{"city":"London"}}
68
+ {"full_name":"Bob","age":35,"address":{"city":"Paris"}}
69
+ JSONL
70
+ end
71
+
72
+ let(:valid_jsonl_with_empty_lines) do
73
+ <<~JSONL
74
+ {"full_name":"John","age":30,"address":{"city":"New York"}}
75
+
76
+ {"full_name":"Jane","age":25,"address":{"city":"London"}}
77
+
78
+
79
+ {"full_name":"Bob","age":35,"address":{"city":"Paris"}}
80
+
81
+
82
+ JSONL
83
+ end
84
+
85
+ let(:invalid_jsonl_content) do
86
+ <<~JSONL
87
+ {"full_name": "John", "age": 30, "address": {"city": "New York"}}
88
+ invalid json
89
+ {"full_name": "Bob", "age": 35, "address": {"city": "Paris"}}
90
+ JSONL
91
+ end
92
+
93
+ let(:parsed) { JsonlSpec::Directory.from_jsonl(valid_jsonl) }
94
+
95
+ it "parses all the json lines" do
96
+ expect(parsed.persons).to eq([john, jane, bob])
97
+ end
98
+
99
+ it "handles empty lines" do
100
+ expect(
101
+ JsonlSpec::Directory.from_jsonl(valid_jsonl_with_empty_lines).persons,
102
+ ).to eq([john, jane, bob])
103
+ end
104
+
105
+ it "round trips valid jsonl correctly" do
106
+ expect(parsed.to_jsonl).to eq(valid_jsonl)
107
+ end
108
+
109
+ it "removes empty line when round triping valid jsonl" do
110
+ expect(
111
+ JsonlSpec::Directory.from_jsonl(valid_jsonl_with_empty_lines).to_jsonl,
112
+ ).to eq(valid_jsonl)
113
+ end
114
+
115
+ it "skips invalid lines and show warning" do
116
+ expect do
117
+ JsonlSpec::Directory.from_jsonl(invalid_jsonl_content)
118
+ end.to output(/Skipping invalid line: unexpected character: /).to_stderr
119
+ end
120
+
121
+ describe "parsing" do
122
+ it "handles empty lines" do
123
+ jsonl_with_empty_lines = <<~JSONL
124
+ {"name": "John", "age": 30, "address": {"city": "New York"}}
125
+
126
+ {"name": "Jane", "age": 25, "address": {"city": "London"}}
127
+ JSONL
128
+ result = JsonlSpec::Directory.from_jsonl(jsonl_with_empty_lines)
129
+ expect(result.persons).to be_an(Array)
130
+ expect(result.persons.length).to eq(2)
131
+ end
132
+ end
133
+
134
+ describe "roundtrip" do
135
+ it "maintains data integrity with special characters" do
136
+ special_chars_jsonl = <<~JSONL
137
+ {"full_name": "John Doe", "age": 30, "address": {"city": "New York"}}
138
+ {"full_name": "Jane \\"Smith\\"", "age": 25, "address": {"city": "London"}}
139
+ JSONL
140
+
141
+ # Parse the original JSONL
142
+ directory = JsonlSpec::Directory.from_jsonl(special_chars_jsonl)
143
+
144
+ # Serialize back to JSONL
145
+ serialized = directory.to_jsonl
146
+
147
+ # Parse the serialized data again
148
+ roundtrip_directory = JsonlSpec::Directory.from_jsonl(serialized)
149
+
150
+ # Compare the results
151
+ expect(roundtrip_directory.persons.length).to eq(directory.persons.length)
152
+ roundtrip_directory.persons.each_with_index do |person, index|
153
+ original = directory.persons[index]
154
+ expect(person.name).to eq(original.name)
155
+ expect(person.age).to eq(original.age)
156
+ expect(person.address.city).to eq(original.address.city)
157
+ end
158
+ end
159
+ end
160
+
161
+ describe "format conversion" do
162
+ let(:valid_jsonl_content) do
163
+ <<~JSONL.strip
164
+ {"full_name":"John","age":30,"address":{"city":"New York"}}
165
+ {"full_name":"Jane","age":25,"address":{"city":"London"}}
166
+ {"full_name":"Bob","age":35,"address":{"city":"Paris"}}
167
+ JSONL
168
+ end
169
+
170
+ it "converts between JSONL and JSON" do
171
+ # Parse JSONL to Directory
172
+ directory = JsonlSpec::Directory.from_jsonl(valid_jsonl_content)
173
+
174
+ # Convert to JSON
175
+ json_string = directory.to_json
176
+
177
+ # Parse JSON back to Directory
178
+ json_directory = JsonlSpec::Directory.from_json(json_string)
179
+
180
+ # Convert back to JSONL
181
+ jsonl_output = json_directory.to_jsonl
182
+
183
+ # Final parse to verify
184
+ final_directory = JsonlSpec::Directory.from_jsonl(jsonl_output)
185
+
186
+ # Compare the results
187
+ expect(final_directory.persons.length).to eq(directory.persons.length)
188
+ final_directory.persons.each_with_index do |person, index|
189
+ original = directory.persons[index]
190
+ expect(person.name).to eq(original.name)
191
+ expect(person.age).to eq(original.age)
192
+ expect(person.address.city).to eq(original.address.city)
193
+ end
194
+ end
195
+
196
+ it "handles collections of objects" do
197
+ # Create a directory with people
198
+ directory = JsonlSpec::Directory.new(
199
+ [
200
+ JsonlSpec::Person.new(
201
+ name: "John",
202
+ age: 30,
203
+ address: JsonlSpec::Address.new(city: "New York"),
204
+ ),
205
+ JsonlSpec::Person.new(
206
+ name: "Jane",
207
+ age: 25,
208
+ address: JsonlSpec::Address.new(city: "London"),
209
+ ),
210
+ ],
211
+ )
212
+
213
+ # Convert to JSONL
214
+ jsonl_output = directory.to_jsonl
215
+
216
+ # Parse back to verify
217
+ parsed_directory = JsonlSpec::Directory.from_jsonl(jsonl_output)
218
+
219
+ # Compare the results
220
+ expect(parsed_directory.persons.length).to eq(directory.persons.length)
221
+ parsed_directory.persons.each_with_index do |person, index|
222
+ original = directory.persons[index]
223
+ expect(person.name).to eq(original.name)
224
+ expect(person.age).to eq(original.age)
225
+ expect(person.address.city).to eq(original.address.city)
226
+ end
227
+ end
228
+ end
229
+ end
@@ -126,7 +126,7 @@ module MultipleMapping
126
126
  end
127
127
 
128
128
  def size_from_xml(model, values)
129
- model.size = (values.first.text.to_i || 0) - 10
129
+ model.size = values.first.text.to_i - 10
130
130
  end
131
131
 
132
132
  def desc_to_xml(model, parent, doc)