lutaml-model 0.7.2 → 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.
- checksums.yaml +4 -4
- data/.envrc +1 -0
- data/.github/workflows/release.yml +3 -0
- data/.gitignore +6 -1
- data/.irbrc +1 -0
- data/.pryrc +1 -0
- data/.rubocop_todo.yml +25 -52
- data/README.adoc +2177 -192
- data/docs/custom_registers.adoc +228 -0
- data/docs/schema_generation.adoc +898 -0
- data/docs/schema_import.adoc +364 -0
- data/flake.lock +114 -0
- data/flake.nix +103 -0
- data/lib/lutaml/model/attribute.rb +230 -94
- data/lib/lutaml/model/choice.rb +30 -0
- data/lib/lutaml/model/collection.rb +194 -0
- data/lib/lutaml/model/comparable_model.rb +3 -3
- data/lib/lutaml/model/config.rb +26 -3
- data/lib/lutaml/model/constants.rb +2 -0
- data/lib/lutaml/model/error/element_count_out_of_range_error.rb +29 -0
- data/lib/lutaml/model/error/invalid_attribute_name_error.rb +15 -0
- data/lib/lutaml/model/error/invalid_attribute_options_error.rb +16 -0
- data/lib/lutaml/model/error/invalid_choice_range_error.rb +3 -5
- data/lib/lutaml/model/error/register/not_registrable_class_error.rb +11 -0
- data/lib/lutaml/model/error/type/invalid_value_error.rb +5 -3
- data/lib/lutaml/model/error/type/max_bound_error.rb +20 -0
- data/lib/lutaml/model/error/type/max_length_error.rb +20 -0
- data/lib/lutaml/model/error/type/min_bound_error.rb +20 -0
- data/lib/lutaml/model/error/type/min_length_error.rb +20 -0
- data/lib/lutaml/model/error/type/pattern_not_matched_error.rb +18 -0
- data/lib/lutaml/model/error/validation_failed_error.rb +9 -0
- data/lib/lutaml/model/error.rb +10 -0
- data/lib/lutaml/model/errors.rb +36 -0
- data/lib/lutaml/model/format_registry.rb +5 -2
- data/lib/lutaml/model/global_register.rb +41 -0
- data/lib/lutaml/model/{hash.rb → hash_adapter.rb} +5 -5
- data/lib/lutaml/model/jsonl/document.rb +14 -0
- data/lib/lutaml/model/jsonl/mapping.rb +19 -0
- data/lib/lutaml/model/jsonl/mapping_rule.rb +9 -0
- data/lib/lutaml/model/jsonl/standard_adapter.rb +33 -0
- data/lib/lutaml/model/jsonl/transform.rb +19 -0
- data/lib/lutaml/model/jsonl.rb +21 -0
- data/lib/lutaml/model/key_value_document.rb +3 -2
- data/lib/lutaml/model/mapping/key_value_mapping.rb +64 -4
- data/lib/lutaml/model/mapping/key_value_mapping_rule.rb +4 -0
- data/lib/lutaml/model/mapping/mapping_rule.rb +8 -3
- data/lib/lutaml/model/register.rb +105 -0
- data/lib/lutaml/model/registrable.rb +6 -0
- data/lib/lutaml/model/schema/base_schema.rb +64 -0
- data/lib/lutaml/model/schema/decorators/attribute.rb +114 -0
- data/lib/lutaml/model/schema/decorators/choices.rb +31 -0
- data/lib/lutaml/model/schema/decorators/class_definition.rb +85 -0
- data/lib/lutaml/model/schema/decorators/definition_collection.rb +97 -0
- data/lib/lutaml/model/schema/generator/definition.rb +53 -0
- data/lib/lutaml/model/schema/generator/definitions_collection.rb +81 -0
- data/lib/lutaml/model/schema/generator/properties_collection.rb +63 -0
- data/lib/lutaml/model/schema/generator/property.rb +110 -0
- data/lib/lutaml/model/schema/generator/ref.rb +24 -0
- data/lib/lutaml/model/schema/helpers/template_helper.rb +49 -0
- data/lib/lutaml/model/schema/json_schema.rb +42 -49
- data/lib/lutaml/model/schema/relaxng_schema.rb +14 -10
- data/lib/lutaml/model/schema/renderer.rb +36 -0
- data/lib/lutaml/model/schema/shared_methods.rb +24 -0
- data/lib/lutaml/model/schema/templates/model.erb +9 -0
- data/lib/lutaml/model/schema/xml_compiler/attribute.rb +85 -0
- data/lib/lutaml/model/schema/xml_compiler/attribute_group.rb +45 -0
- data/lib/lutaml/model/schema/xml_compiler/choice.rb +65 -0
- data/lib/lutaml/model/schema/xml_compiler/complex_content.rb +27 -0
- data/lib/lutaml/model/schema/xml_compiler/complex_content_restriction.rb +34 -0
- data/lib/lutaml/model/schema/xml_compiler/complex_type.rb +136 -0
- data/lib/lutaml/model/schema/xml_compiler/element.rb +104 -0
- data/lib/lutaml/model/schema/xml_compiler/group.rb +97 -0
- data/lib/lutaml/model/schema/xml_compiler/restriction.rb +101 -0
- data/lib/lutaml/model/schema/xml_compiler/sequence.rb +50 -0
- data/lib/lutaml/model/schema/xml_compiler/simple_content.rb +36 -0
- data/lib/lutaml/model/schema/xml_compiler/simple_type.rb +189 -0
- data/lib/lutaml/model/schema/xml_compiler.rb +231 -587
- data/lib/lutaml/model/schema/xsd_schema.rb +12 -8
- data/lib/lutaml/model/schema/yaml_schema.rb +41 -35
- data/lib/lutaml/model/schema.rb +1 -0
- data/lib/lutaml/model/sequence.rb +60 -30
- data/lib/lutaml/model/serialize.rb +175 -53
- data/lib/lutaml/model/services/base.rb +11 -0
- data/lib/lutaml/model/services/logger.rb +2 -2
- data/lib/lutaml/model/services/rule_value_extractor.rb +92 -0
- data/lib/lutaml/model/services/type/validator/number.rb +25 -0
- data/lib/lutaml/model/services/type/validator/string.rb +52 -0
- data/lib/lutaml/model/services/type/validator.rb +43 -0
- data/lib/lutaml/model/services/validator.rb +145 -0
- data/lib/lutaml/model/services.rb +3 -0
- data/lib/lutaml/model/transform/key_value_transform.rb +60 -50
- data/lib/lutaml/model/transform/xml_transform.rb +46 -57
- data/lib/lutaml/model/transform.rb +22 -8
- data/lib/lutaml/model/type/boolean.rb +1 -1
- data/lib/lutaml/model/type/date.rb +1 -1
- data/lib/lutaml/model/type/date_time.rb +1 -1
- data/lib/lutaml/model/type/decimal.rb +11 -9
- data/lib/lutaml/model/type/float.rb +2 -1
- data/lib/lutaml/model/type/integer.rb +24 -21
- data/lib/lutaml/model/type/string.rb +4 -2
- data/lib/lutaml/model/type/time.rb +1 -1
- data/lib/lutaml/model/type/time_without_date.rb +1 -1
- data/lib/lutaml/model/type/value.rb +5 -1
- data/lib/lutaml/model/type.rb +5 -2
- data/lib/lutaml/model/utils.rb +30 -8
- data/lib/lutaml/model/validation.rb +6 -4
- data/lib/lutaml/model/version.rb +1 -1
- data/lib/lutaml/model/xml/document.rb +37 -19
- data/lib/lutaml/model/xml/mapping.rb +74 -13
- data/lib/lutaml/model/xml/mapping_rule.rb +10 -2
- data/lib/lutaml/model/xml/nokogiri_adapter.rb +5 -3
- data/lib/lutaml/model/xml/oga/element.rb +4 -1
- data/lib/lutaml/model/xml/oga_adapter.rb +4 -3
- data/lib/lutaml/model/xml/ox_adapter.rb +20 -6
- data/lib/lutaml/model/xml/xml_element.rb +3 -28
- data/lib/lutaml/model/xml.rb +1 -1
- data/lib/lutaml/model/xml_adapter/element.rb +1 -1
- data/lib/lutaml/model/xml_adapter/nokogiri_adapter.rb +1 -1
- data/lib/lutaml/model/xml_adapter/oga_adapter.rb +1 -1
- data/lib/lutaml/model/xml_adapter/ox_adapter.rb +1 -1
- data/lib/lutaml/model/yamls/document.rb +14 -0
- data/lib/lutaml/model/yamls/mapping.rb +19 -0
- data/lib/lutaml/model/yamls/mapping_rule.rb +9 -0
- data/lib/lutaml/model/yamls/standard_adapter.rb +34 -0
- data/lib/lutaml/model/yamls/transform.rb +19 -0
- data/lib/lutaml/model/yamls.rb +21 -0
- data/lib/lutaml/model.rb +7 -31
- data/spec/benchmarks/xml_parsing_benchmark_spec.rb +4 -5
- data/spec/fixtures/xml/advanced_test_schema.xsd +134 -0
- data/spec/fixtures/xml/examples/nested_categories.xml +55 -0
- data/spec/fixtures/xml/examples/valid_catalog.xml +43 -0
- data/spec/fixtures/xml/product_catalog.xsd +151 -0
- data/spec/fixtures/xml/specifications_schema.xsd +38 -0
- data/spec/lutaml/model/attribute_collection_spec.rb +101 -0
- data/spec/lutaml/model/attribute_spec.rb +41 -44
- data/spec/lutaml/model/choice_spec.rb +44 -0
- data/spec/lutaml/model/custom_collection_spec.rb +830 -0
- data/spec/lutaml/model/custom_model_spec.rb +15 -3
- data/spec/lutaml/model/custom_vobject_adapter_spec.rb +6 -6
- data/spec/lutaml/model/defaults_spec.rb +5 -1
- data/spec/lutaml/model/global_register_spec.rb +108 -0
- data/spec/lutaml/model/group_spec.rb +9 -3
- data/spec/lutaml/model/jsonl/standard_adapter_spec.rb +91 -0
- data/spec/lutaml/model/jsonl_spec.rb +229 -0
- data/spec/lutaml/model/multiple_mapping_spec.rb +1 -1
- data/spec/lutaml/model/register/key_value_spec.rb +275 -0
- data/spec/lutaml/model/register/xml_spec.rb +187 -0
- data/spec/lutaml/model/register_spec.rb +147 -0
- data/spec/lutaml/model/rule_value_extractor_spec.rb +162 -0
- data/spec/lutaml/model/schema/generator/definitions_collection_spec.rb +120 -0
- data/spec/lutaml/model/schema/json_schema_spec.rb +412 -51
- data/spec/lutaml/model/schema/json_schema_to_models_spec.rb +383 -0
- data/spec/lutaml/model/schema/xml_compiler/attribute_group_spec.rb +65 -0
- data/spec/lutaml/model/schema/xml_compiler/attribute_spec.rb +63 -0
- data/spec/lutaml/model/schema/xml_compiler/choice_spec.rb +71 -0
- data/spec/lutaml/model/schema/xml_compiler/complex_content_restriction_spec.rb +55 -0
- data/spec/lutaml/model/schema/xml_compiler/complex_content_spec.rb +37 -0
- data/spec/lutaml/model/schema/xml_compiler/complex_type_spec.rb +173 -0
- data/spec/lutaml/model/schema/xml_compiler/element_spec.rb +63 -0
- data/spec/lutaml/model/schema/xml_compiler/group_spec.rb +86 -0
- data/spec/lutaml/model/schema/xml_compiler/restriction_spec.rb +76 -0
- data/spec/lutaml/model/schema/xml_compiler/sequence_spec.rb +59 -0
- data/spec/lutaml/model/schema/xml_compiler/simple_content_spec.rb +55 -0
- data/spec/lutaml/model/schema/xml_compiler/simple_type_spec.rb +181 -0
- data/spec/lutaml/model/schema/xml_compiler_spec.rb +503 -1804
- data/spec/lutaml/model/schema/yaml_schema_spec.rb +249 -26
- data/spec/lutaml/model/sequence_spec.rb +36 -0
- data/spec/lutaml/model/serializable_spec.rb +31 -0
- data/spec/lutaml/model/type_spec.rb +8 -4
- data/spec/lutaml/model/utils_spec.rb +3 -3
- data/spec/lutaml/model/xml/derived_attributes_spec.rb +1 -1
- data/spec/lutaml/model/xml/xml_element_spec.rb +7 -1
- data/spec/lutaml/model/xml_adapter/xml_namespace_spec.rb +6 -6
- data/spec/lutaml/model/xml_adapter_spec.rb +24 -0
- data/spec/lutaml/model/xml_mapping_rule_spec.rb +11 -4
- data/spec/lutaml/model/xml_mapping_spec.rb +1 -1
- data/spec/lutaml/model/xml_spec.rb +6 -6
- data/spec/lutaml/model/yamls/standard_adapter_spec.rb +183 -0
- data/spec/lutaml/model/yamls_spec.rb +294 -0
- data/spec/spec_helper.rb +1 -0
- metadata +105 -9
- data/lib/lutaml/model/schema/templates/simple_type.rb +0 -247
- /data/lib/lutaml/model/{hash → hash_adapter}/document.rb +0 -0
- /data/lib/lutaml/model/{hash → hash_adapter}/mapping.rb +0 -0
- /data/lib/lutaml/model/{hash → hash_adapter}/mapping_rule.rb +0 -0
- /data/lib/lutaml/model/{hash → hash_adapter}/standard_adapter.rb +0 -0
- /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
|
@@ -116,7 +116,7 @@ require "spec_helper"
|
|
116
116
|
|
117
117
|
require_relative "../../../lib/lutaml/model/serialization_adapter"
|
118
118
|
|
119
|
-
module
|
119
|
+
module CustomVobjectAdapterSpec
|
120
120
|
class VobjectParser
|
121
121
|
def initialize(data)
|
122
122
|
@data = data
|
@@ -994,7 +994,7 @@ module CustomVojectAdapterSpec
|
|
994
994
|
described_class.new(
|
995
995
|
version: "4.0",
|
996
996
|
fn: "John Doe",
|
997
|
-
n:
|
997
|
+
n: CustomVobjectAdapterSpec::VcardName.new(
|
998
998
|
family: "Doe",
|
999
999
|
given: "John",
|
1000
1000
|
additional: "Middle",
|
@@ -1002,13 +1002,13 @@ module CustomVojectAdapterSpec
|
|
1002
1002
|
suffix: "PhD",
|
1003
1003
|
),
|
1004
1004
|
tel: [
|
1005
|
-
|
1006
|
-
|
1005
|
+
CustomVobjectAdapterSpec::VcardTel.new(value: "tel:+1-555-555-5555"),
|
1006
|
+
CustomVobjectAdapterSpec::VcardTel.new(value: "tel:+1-555-555-1234"),
|
1007
1007
|
],
|
1008
1008
|
email: ["john.doe@example.com", "j.doe@company.com"],
|
1009
1009
|
org: "Example Corp",
|
1010
|
-
bday:
|
1011
|
-
value:
|
1010
|
+
bday: CustomVobjectAdapterSpec::VcardBday.new(
|
1011
|
+
value: CustomVobjectAdapterSpec::VobjectPropertyValue.parse("1970-01-01", "DATE-AND-OR-TIME"),
|
1012
1012
|
),
|
1013
1013
|
)
|
1014
1014
|
end
|
@@ -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
|
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
|
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(
|
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
|