lutaml-model 0.8.23 → 0.8.28

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 (73) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/performance.yml +1 -1
  3. data/Gemfile +6 -0
  4. data/README.adoc +16 -7
  5. data/docs/_guides/index.adoc +4 -0
  6. data/docs/_guides/native-engines.adoc +61 -0
  7. data/docs/_pages/configuration.adoc +11 -7
  8. data/docs/_pages/serialization_adapters.adoc +14 -5
  9. data/lib/compat/opal/lutaml_model_boot.rb +5 -0
  10. data/lib/lutaml/json/adapter/multi_json_adapter.rb +12 -1
  11. data/lib/lutaml/json/adapter/yeptris_adapter.rb +21 -0
  12. data/lib/lutaml/json/adapter.rb +1 -0
  13. data/lib/lutaml/json/format.rb +1 -1
  14. data/lib/lutaml/key_value/adapter/json/multi_json_adapter.rb +16 -1
  15. data/lib/lutaml/key_value/adapter/json/yeptris_adapter.rb +15 -0
  16. data/lib/lutaml/key_value/adapter/toml/teptris_adapter.rb +42 -0
  17. data/lib/lutaml/key_value/adapter/toml.rb +1 -0
  18. data/lib/lutaml/key_value/adapter/yaml/yeptris_adapter.rb +15 -0
  19. data/lib/lutaml/key_value/transform.rb +82 -0
  20. data/lib/lutaml/key_value/transformation/value_serializer.rb +25 -0
  21. data/lib/lutaml/key_value/transformation.rb +25 -1
  22. data/lib/lutaml/model/adapter_resolver.rb +23 -6
  23. data/lib/lutaml/model/attribute.rb +31 -6
  24. data/lib/lutaml/model/compiled_rule.rb +16 -0
  25. data/lib/lutaml/model/configuration.rb +5 -0
  26. data/lib/lutaml/model/error/unknown_adapter_type_error.rb +4 -4
  27. data/lib/lutaml/model/serialize/format_conversion.rb +65 -0
  28. data/lib/lutaml/model/toml.rb +10 -6
  29. data/lib/lutaml/model/type/date_time.rb +2 -2
  30. data/lib/lutaml/model/type/value.rb +24 -2
  31. data/lib/lutaml/model/utils.rb +6 -5
  32. data/lib/lutaml/model/version.rb +1 -1
  33. data/lib/lutaml/toml/adapter/teptris_adapter.rb +14 -0
  34. data/lib/lutaml/toml/adapter.rb +1 -0
  35. data/lib/lutaml/toml/format.rb +1 -1
  36. data/lib/lutaml/xml/adapter/leptris_adapter.rb +15 -0
  37. data/lib/lutaml/xml/adapter_element.rb +111 -3
  38. data/lib/lutaml/xml/builder/base.rb +3 -1
  39. data/lib/lutaml/xml/builder/leptris.rb +13 -0
  40. data/lib/lutaml/xml/builder/nokogiri.rb +3 -0
  41. data/lib/lutaml/xml/builder.rb +1 -0
  42. data/lib/lutaml/xml/declaration_planner.rb +8 -2
  43. data/lib/lutaml/xml/format.rb +7 -1
  44. data/lib/lutaml/xml/leptris/element.rb +34 -0
  45. data/lib/lutaml/xml/namespace_collector.rb +4 -3
  46. data/lib/lutaml/xml/plan_compiler.rb +253 -0
  47. data/lib/lutaml/xml/plan_hydrator.rb +341 -0
  48. data/lib/lutaml/xml/plan_order.rb +50 -0
  49. data/lib/lutaml/xml/plan_serializer.rb +241 -0
  50. data/lib/lutaml/xml/plan_walk.rb +30 -0
  51. data/lib/lutaml/xml/transformation/custom_method_wrapper.rb +8 -0
  52. data/lib/lutaml/xml/xml_element.rb +6 -2
  53. data/lib/lutaml/yaml/adapter/yeptris_adapter.rb +46 -0
  54. data/lib/lutaml/yaml/adapter.rb +5 -0
  55. data/lib/lutaml/yaml/format.rb +1 -1
  56. data/lib/tasks/performance.rake +5 -0
  57. data/lib/tasks/performance_comparator.rb +19 -3
  58. data/lib/tasks/performance_probe.rake +33 -0
  59. data/lib/tasks/performance_probe.rb +106 -0
  60. data/lib/tasks/performance_probe_plan_walk.rb +58 -0
  61. data/lib/tasks/performance_probe_xml.rb +54 -0
  62. data/lutaml-model.gemspec +1 -1
  63. data/spec/lutaml/json/yeptris_adapter_spec.rb +63 -0
  64. data/spec/lutaml/key_value/adapter/toml/teptris_adapter_spec.rb +71 -0
  65. data/spec/lutaml/model/json_adapter_spec.rb +6 -0
  66. data/spec/lutaml/model/toml_adapter_spec.rb +8 -0
  67. data/spec/lutaml/model/toml_spec.rb +36 -1
  68. data/spec/lutaml/xml/plan_fast_path_spec.rb +547 -0
  69. data/spec/lutaml/xml/plan_walk_spec.rb +102 -0
  70. data/spec/lutaml/xml/xml_spec.rb +13 -0
  71. data/spec/lutaml/yaml/yeptris_adapter_spec.rb +63 -0
  72. data/spec/spec_helper.rb +7 -1
  73. metadata +28 -4
data/lutaml-model.gemspec CHANGED
@@ -38,7 +38,7 @@ Gem::Specification.new do |spec|
38
38
  spec.add_dependency "canon"
39
39
  spec.add_dependency "concurrent-ruby"
40
40
  spec.add_dependency "liquid", ">= 4.0", "< 6.0"
41
- spec.add_dependency "moxml", "~> 0.5.0"
41
+ spec.add_dependency "moxml", "~> 0.5.56"
42
42
  spec.add_dependency "ostruct"
43
43
  spec.add_dependency "rubyzip", "~> 2.3"
44
44
  spec.add_dependency "thor"
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+ begin
5
+ require "lutaml/json/adapter/yeptris_adapter"
6
+ rescue LoadError
7
+ # yeptris ships no platform gem here (e.g. Windows): every example skips.
8
+ end
9
+
10
+ YEPTRIS_AVAILABLE = defined?(Yeptris)
11
+
12
+ RSpec.describe(YEPTRIS_AVAILABLE ? Lutaml::Json::Adapter::YeptrisAdapter : Object) do
13
+ before { skip "yeptris is not available on this platform" unless YEPTRIS_AVAILABLE }
14
+
15
+ let(:attributes) { { "name" => "John", "age" => 30, "roles" => %w[admin dev] } }
16
+
17
+ describe ".parse" do
18
+ it "parses a JSON document into a hash" do
19
+ expect(described_class.parse('{"name":"John","age":30,"roles":["admin","dev"]}'))
20
+ .to eq(attributes)
21
+ end
22
+
23
+ it "parses scalars and nulls like JSON.parse" do
24
+ expect(described_class.parse('{"a":1.5,"b":null,"c":true,"d":"1e3"}'))
25
+ .to eq("a" => 1.5, "b" => nil, "c" => true, "d" => "1e3")
26
+ end
27
+
28
+ it "raises on invalid JSON" do
29
+ expect { described_class.parse("{nope") }
30
+ .to raise_error(Yeptris::JSON::ParseError)
31
+ end
32
+ end
33
+
34
+ describe "#to_json" do
35
+ subject(:document) { described_class.new(attributes) }
36
+
37
+ it "serializes via the inherited json generator" do
38
+ expect(document.to_json)
39
+ .to eq('{"name":"John","age":30,"roles":["admin","dev"]}')
40
+ end
41
+ end
42
+
43
+ describe "round trip through a model" do
44
+ let(:model_class) do
45
+ Class.new(Lutaml::Model::Serializable) do
46
+ attribute :name, :string
47
+ attribute :roles, :string, collection: true
48
+
49
+ key_value do
50
+ map "name", to: :name
51
+ map "roles", to: :roles
52
+ end
53
+ end
54
+ end
55
+
56
+ it "deserializes via the yeptris engine and serializes back" do
57
+ model = model_class.from_json('{"name":"John","roles":["admin","dev"]}')
58
+ expect(model.name).to eq("John")
59
+ expect(model.roles).to eq(%w[admin dev])
60
+ expect(model_class.from_json(model.to_json).roles).to eq(%w[admin dev])
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+ require "lutaml/key_value/adapter/toml/teptris_adapter"
5
+ begin
6
+ require "teptris"
7
+ rescue LoadError
8
+ # teptris is not resolvable here (absent or broken native library):
9
+ # every example skips.
10
+ end
11
+
12
+ TEPTRIS_AVAILABLE = defined?(Teptris)
13
+
14
+ RSpec.describe(TEPTRIS_AVAILABLE ? Lutaml::KeyValue::Adapter::Toml::TeptrisAdapter : Object) do
15
+ before { skip "teptris is not available" unless TEPTRIS_AVAILABLE }
16
+
17
+ describe ".parse" do
18
+ it "parses a TOML document into a hash" do
19
+ expect(described_class.parse("name = \"John\"\nage = 30\n"))
20
+ .to eq("name" => "John", "age" => 30)
21
+ end
22
+
23
+ it "parses nested tables and arrays" do
24
+ toml = <<~TOML
25
+ title = "cfg"
26
+ [owner]
27
+ name = "t"
28
+ tags = ["a", "b"]
29
+ TOML
30
+ expect(described_class.parse(toml))
31
+ .to eq("title" => "cfg", "owner" => { "name" => "t", "tags" => %w[a b] })
32
+ end
33
+
34
+ it "materializes dates and datetimes like tomlib" do
35
+ require "date"
36
+ doc = described_class.parse("d = 1979-05-27\ndt = 1979-05-27T07:32:00Z\n")
37
+ expect(doc["d"]).to be_a(Date)
38
+ expect(doc["dt"]).to be_a(Time)
39
+ end
40
+
41
+ it "reports parse errors with line and column" do
42
+ expect { described_class.parse("a = [1,") }
43
+ .to raise_error(Teptris::ParseError) do |e|
44
+ expect(e.line).to eq(1)
45
+ expect(e.column).to eq(8)
46
+ end
47
+ end
48
+ end
49
+
50
+ describe "#to_toml" do
51
+ it "dumps a hash to TOML" do
52
+ attributes = { "name" => "x", "nested" => { "a" => [1, 2] } }
53
+ expect(described_class.new(attributes).to_toml)
54
+ .to include("name", "[nested]", "a = [1, 2]")
55
+ end
56
+
57
+ it "unwraps the __root__ KeyValueElement wrapper" do
58
+ element = Lutaml::KeyValue::DataModel::Element.new("__root__")
59
+ name_child = Lutaml::KeyValue::DataModel::Element.new("name")
60
+ name_child.value = "x"
61
+ element.add_child(name_child)
62
+ expect(described_class.new(element).to_toml).to include("name")
63
+ end
64
+ end
65
+
66
+ it "round-trips through the model layer" do
67
+ attributes = { "name" => "John", "age" => 30 }
68
+ adapter_toml = described_class.new(attributes).to_toml
69
+ expect(described_class.parse(adapter_toml)).to eq(attributes)
70
+ end
71
+ end
@@ -1,4 +1,5 @@
1
1
  require "spec_helper"
2
+ require "multi_json"
2
3
  require "lutaml/key_value/adapter/json/standard_adapter"
3
4
  require "lutaml/key_value/adapter/json/multi_json_adapter"
4
5
  require "lutaml/key_value/adapter/json/oj_adapter"
@@ -37,6 +38,11 @@ RSpec.describe "JsonAdapter" do
37
38
  end
38
39
 
39
40
  describe Lutaml::KeyValue::Adapter::Json::MultiJsonAdapter do
41
+ # multi_json 1.21's json_gem adapter passes options positionally to
42
+ # JSON.parse, which json >= 3 removed (intridea/multi_json#227).
43
+ if Gem::Version.new(JSON::VERSION) >= Gem::Version.new("3")
44
+ before { skip "multi_json is incompatible with json #{JSON::VERSION} (intridea/multi_json#227)" }
45
+ end
40
46
  it_behaves_like "a JSON adapter", described_class
41
47
  end
42
48
 
@@ -17,6 +17,8 @@ RSpec.describe "TomlAdapter" do
17
17
  TomlRB.dump(attributes)
18
18
  elsif adapter_class == Lutaml::Model::Toml::TomlibAdapter
19
19
  Tomlib.dump(attributes)
20
+ elsif adapter_class == Lutaml::Model::Toml::TeptrisAdapter && defined?(Teptris)
21
+ Teptris::TOML.dump(attributes)
20
22
  end
21
23
  end
22
24
 
@@ -45,4 +47,10 @@ RSpec.describe "TomlAdapter" do
45
47
  it_behaves_like "a TOML adapter", described_class
46
48
  end
47
49
  end
50
+
51
+ if defined?(Teptris)
52
+ describe Lutaml::Model::Toml::TeptrisAdapter do
53
+ it_behaves_like "a TOML adapter", described_class
54
+ end
55
+ end
48
56
  end
@@ -10,7 +10,7 @@ RSpec.describe Lutaml::Model::Toml do
10
10
  end
11
11
 
12
12
  it "does not autoload native TOML adapters on Opal" do
13
- %i[TomlRbAdapter TomlibAdapter].each do |constant_name|
13
+ %i[TomlRbAdapter TomlibAdapter TeptrisAdapter].each do |constant_name|
14
14
  hide_const("Lutaml::Toml::Adapter::#{constant_name}")
15
15
  end
16
16
 
@@ -19,12 +19,14 @@ RSpec.describe Lutaml::Model::Toml do
19
19
 
20
20
  expect(Lutaml::Toml::Adapter.autoload?(:TomlRbAdapter)).to be_nil
21
21
  expect(Lutaml::Toml::Adapter.autoload?(:TomlibAdapter)).to be_nil
22
+ expect(Lutaml::Toml::Adapter.autoload?(:TeptrisAdapter)).to be_nil
22
23
  end
23
24
  end
24
25
 
25
26
  describe ".detect_toml_adapter" do
26
27
  before do
27
28
  # Hide any existing constants first
29
+ hide_const("Teptris") if Object.const_defined?(:Teptris)
28
30
  hide_const("Tomlib") if Object.const_defined?(:Tomlib)
29
31
  hide_const("TomlRb") if Object.const_defined?(:TomlRb)
30
32
 
@@ -32,6 +34,39 @@ RSpec.describe Lutaml::Model::Toml do
32
34
  allow(Lutaml::Model::Utils).to receive(:require).and_return(true)
33
35
  end
34
36
 
37
+ context "when teptris is available" do
38
+ before do
39
+ stub_const("Teptris", Module.new)
40
+ end
41
+
42
+ it "returns :teptris on non-Windows platforms (tomlib absent)" do
43
+ allow(Gem).to receive(:win_platform?).and_return(false)
44
+ expect(described_class.detect_toml_adapter).to eq(:teptris)
45
+ end
46
+
47
+ it "returns :teptris on Windows (native TOML without tomlib segfaults)" do
48
+ allow(Gem).to receive(:win_platform?).and_return(true)
49
+ expect(described_class.detect_toml_adapter).to eq(:teptris)
50
+ end
51
+ end
52
+
53
+ context "when teptris and tomlib are both available" do
54
+ before do
55
+ stub_const("Teptris", Module.new)
56
+ stub_const("Tomlib", Module.new)
57
+ end
58
+
59
+ it "prefers teptris on non-Windows (0.2.12+ beats tomlib both directions)" do
60
+ allow(Gem).to receive(:win_platform?).and_return(false)
61
+ expect(described_class.detect_toml_adapter).to eq(:teptris)
62
+ end
63
+
64
+ it "prefers teptris on Windows" do
65
+ allow(Gem).to receive(:win_platform?).and_return(true)
66
+ expect(described_class.detect_toml_adapter).to eq(:teptris)
67
+ end
68
+ end
69
+
35
70
  context "when Tomlib is available" do
36
71
  before do
37
72
  stub_const("Tomlib", Module.new)