lutaml-model 0.6.2 → 0.6.4

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 94d3957e15a5f50e2fc02c922f739acf71e99a592b97c4461c64b20cbc203bc5
4
- data.tar.gz: 0daf15d9e99b94ecce154a95b86d8b24a11bc4ae89f7e21f44cd7d251ec11ab9
3
+ metadata.gz: 16b8569b5f2b450dfa8218c1c55a2175f154790220a54536bc9acbf6bc718312
4
+ data.tar.gz: d9d62e793690a72856e2663a67eb4eeb9cd8b909a861a91b093d8b849674a983
5
5
  SHA512:
6
- metadata.gz: a05e07d0948529796da6833032c9edae933a022734f9018fc459a4caf7e8ee6d475e325b002a58c4b5de764fecde077380ab528bc0a60f87eecef8919dfa16f4
7
- data.tar.gz: f6116e02319e52a64e6ed0817655b456ca0be1933f66fea70dab1999fbd4a1c2a70b52cfc60f153d1b18d6778b23882598c76ca7a4af0b7e7eb5eca0e563040c
6
+ metadata.gz: 5e491d76d913445b9929b4e1939becc31c4b1ccb656aeaaa3e83caa20bf3c5e53142c3cc45f1a82b0a85d4b7111ab6994d5a5c03588c4f42dd8f76d101c95db0
7
+ data.tar.gz: e18a44f0047929b93c2bc66c82f8942d3cf48818c6597722bba0b78107c2630aa59f84b3b4034a105155d889e64fa7e8f11881fbc9eca076b3103c21f8f189cf
data/.rubocop_todo.yml CHANGED
@@ -1,6 +1,6 @@
1
1
  # This configuration was generated by
2
2
  # `rubocop --auto-gen-config`
3
- # on 2025-02-13 10:44:38 UTC using RuboCop version 1.71.2.
3
+ # on 2025-02-15 02:54:02 UTC using RuboCop version 1.71.2.
4
4
  # The point is for the user to remove these configuration records
5
5
  # one by one as the offenses are removed from the code base.
6
6
  # Note that changes in the inspected code, or installation of new
@@ -138,7 +138,7 @@ RSpec/DescribedClass:
138
138
  Exclude:
139
139
  - 'spec/lutaml/model/xml_mapping_spec.rb'
140
140
 
141
- # Offense count: 152
141
+ # Offense count: 157
142
142
  # Configuration parameters: CountAsOne.
143
143
  RSpec/ExampleLength:
144
144
  Max: 54
@@ -149,7 +149,7 @@ RSpec/LeakyConstantDeclaration:
149
149
  - 'spec/benchmarks/xml_parsing_benchmark_spec.rb'
150
150
  - 'spec/lutaml/model/xml_adapter/xml_namespace_spec.rb'
151
151
 
152
- # Offense count: 206
152
+ # Offense count: 208
153
153
  RSpec/MultipleExpectations:
154
154
  Max: 14
155
155
 
@@ -163,13 +163,12 @@ RSpec/MultipleMemoizedHelpers:
163
163
  RSpec/NestedGroups:
164
164
  Max: 4
165
165
 
166
- # Offense count: 8
166
+ # Offense count: 5
167
167
  RSpec/PendingWithoutReason:
168
168
  Exclude:
169
169
  - 'spec/lutaml/model/type/date_time_spec.rb'
170
170
  - 'spec/lutaml/model/type/time_spec.rb'
171
171
  - 'spec/lutaml/model/type/time_without_date_spec.rb'
172
- - 'spec/lutaml/model/validation_spec.rb'
173
172
 
174
173
  # Offense count: 3
175
174
  # Configuration parameters: Include, CustomTransform, IgnoreMethods, IgnoreMetadata.
data/Gemfile CHANGED
@@ -8,6 +8,7 @@ gemspec
8
8
  gem "benchmark-ips"
9
9
  gem "bigdecimal"
10
10
  gem "equivalent-xml"
11
+ gem "liquid"
11
12
  gem "lutaml-xsd"
12
13
  gem "multi_json"
13
14
  gem "nokogiri"
data/README.adoc CHANGED
@@ -413,9 +413,11 @@ end
413
413
 
414
414
  ==== Decimal type
415
415
 
416
- The Decimal type is an optional type that is disabled by default.
416
+ WARNING: Decimal is an optional feature.
417
417
 
418
- NOTE: The reason why the Decimal type is disalbed by default is that the
418
+ The Decimal type is a value type that is disabled by default.
419
+
420
+ NOTE: The reason why the Decimal type is disabled by default is that the
419
421
  `BigDecimal` class became optional to the standard Ruby library from Ruby 3.4
420
422
  onwards. The `Decimal` type is only enabled when the `bigdecimal` library is
421
423
  loaded.
@@ -693,7 +695,7 @@ end
693
695
  Lutaml lets you create reusable element and attribute collections using `no_root`. These can be imported into other models using:
694
696
 
695
697
  - `import_model`: imports both attributes and mappings
696
- - `import_model_attributes`: imports only attributes
698
+ - `import_model_attributes`: imports only attributes
697
699
  - `import_model_mappings`: imports only mappings
698
700
 
699
701
  NOTE: This feature works with XML. Import order determines how elements and attributes are overwritten.
@@ -3898,7 +3900,7 @@ class Person < Lutaml::Model::Serializable
3898
3900
  }
3899
3901
  end
3900
3902
 
3901
- # Mapping-level transformation in XML format
3903
+ # Mapping-level transformation in XML format
3902
3904
  xml do
3903
3905
  map "full-name", to: :name, transform: {
3904
3906
  export: ->(value) { "Dr. #{value}" },
@@ -4098,39 +4100,39 @@ NOTE: For `NokogiriAdapter`, we can also call `to_xml` on `value.node.adapter_no
4098
4100
 
4099
4101
  # Nokogiri Adapter Node
4100
4102
 
4101
- #<Lutaml::Model::XmlAdapter::NokogiriElement:0x0000000107656ed8
4102
- # @attributes={},
4103
- # @children=
4103
+ #<Lutaml::Model::XmlAdapter::NokogiriElement:0x0000000107656ed8
4104
+ # @attributes={},
4105
+ # @children=
4104
4106
  # [#<Lutaml::Model::XmlAdapter::NokogiriElement:0x0000000107656cd0 @attributes={}, @children=[], @default_namespace=nil, @name="text", @namespace_prefix=nil, @text="\n ">,
4105
- # #<Lutaml::Model::XmlAdapter::NokogiriElement:0x00000001076569b0
4106
- # @attributes={},
4107
- # @children=
4107
+ # #<Lutaml::Model::XmlAdapter::NokogiriElement:0x00000001076569b0
4108
+ # @attributes={},
4109
+ # @children=
4108
4110
  # [#<Lutaml::Model::XmlAdapter::NokogiriElement:0x00000001076567f8 @attributes={}, @children=[], @default_namespace=nil, @name="text", @namespace_prefix=nil, @text="Metadata">],
4109
- # @default_namespace=nil,
4110
- # @name="category",
4111
- # @namespace_prefix=nil,
4112
- # @text="Metadata">,
4111
+ # @default_namespace=nil,
4112
+ # @name="category",
4113
+ # @namespace_prefix=nil,
4114
+ # @text="Metadata">,
4113
4115
  # #<Lutaml::Model::XmlAdapter::NokogiriElement:0x0000000107656028 @attributes={}, @children=[], @default_namespace=nil, @name="text", @namespace_prefix=nil, @text="\n ">],
4114
- # @default_namespace=nil,
4116
+ # @default_namespace=nil,
4115
4117
  # @name="metadata",
4116
4118
  # @namespace_prefix=nil,
4117
4119
  # @text="\n Metadata\n ">
4118
4120
 
4119
4121
  # Ox Adapter Node
4120
4122
 
4121
- #<Lutaml::Model::XmlAdapter::OxElement:0x0000000107584f78
4122
- # @attributes={},
4123
- # @children=
4124
- # [#<Lutaml::Model::XmlAdapter::OxElement:0x0000000107584e60
4125
- # @attributes={},
4123
+ #<Lutaml::Model::XmlAdapter::OxElement:0x0000000107584f78
4124
+ # @attributes={},
4125
+ # @children=
4126
+ # [#<Lutaml::Model::XmlAdapter::OxElement:0x0000000107584e60
4127
+ # @attributes={},
4126
4128
  # @children=[#<Lutaml::Model::XmlAdapter::OxElement:0x0000000107584d48 @attributes={}, @children=[], @default_namespace=nil, @name="text", @namespace_prefix=nil, @text="Metadata">],
4127
- # @default_namespace=nil,
4128
- # @name="category",
4129
- # @namespace_prefix=nil,
4130
- # @text="Metadata">],
4131
- # @default_namespace=nil,
4132
- # @name="metadata",
4133
- # @namespace_prefix=nil,
4129
+ # @default_namespace=nil,
4130
+ # @name="category",
4131
+ # @namespace_prefix=nil,
4132
+ # @text="Metadata">],
4133
+ # @default_namespace=nil,
4134
+ # @name="metadata",
4135
+ # @namespace_prefix=nil,
4134
4136
  # @text=nil>
4135
4137
 
4136
4138
  # Oga Adapter Node
@@ -4190,7 +4192,7 @@ class CustomModelParentMapper < Lutaml::Model::Serializable
4190
4192
  map_element :CustomModelChild,
4191
4193
  with: { to: :child_to_xml, from: :child_from_xml }
4192
4194
  end
4193
-
4195
+
4194
4196
  def child_to_xml(model, parent, doc)
4195
4197
  child_el = doc.create_element("CustomModelChild")
4196
4198
  street_el = doc.create_element("street")
@@ -4227,7 +4229,7 @@ end
4227
4229
  [source,ruby]
4228
4230
  ----
4229
4231
  > instance = CustomModelParentMapper.from_xml(xml)
4230
- > #<CustomModelParent:0x0000000107c9ca68 @child_mapper=#<CustomModelChild:0x0000000107c95218 @city="London", @street="Oxford Street">, @first_name="John">
4232
+ > #<CustomModelParent:0x0000000107c9ca68 @child_mapper=#<CustomModelChild:0x0000000107c95218 @city="London", @street="Oxford Street">, @first_name="John">
4231
4233
  > CustomModelParentMapper.to_xml(instance)
4232
4234
  > #<CustomModelParent><first_name>John</first_name><CustomModelChild><street>Oxford Street</street><city>London</city></CustomModelChild></CustomModelParent>
4233
4235
  ----
@@ -4595,27 +4597,228 @@ klin.validate
4595
4597
  ====
4596
4598
 
4597
4599
 
4598
- == Liquid Compatability
4600
+ == Liquid template access
4601
+
4602
+ WARNING: The Liquid template feature is optional. To enable it, please
4603
+ explicitly require the `liquid` gem.
4604
+
4605
+ The https://shopify.github.io/liquid/[Liquid template language] is an
4606
+ open-source template language developed by Shopify and written in Ruby.
4607
+
4608
+ `Lutaml::Model::Serializable` objects can be safely accessed within Liquid
4609
+ templates through a `to_liquid` method that converts the objects into
4610
+ `Liquid::Drop` instances.
4599
4611
 
4600
- `to_liquid` can be used to convert a class that inherit from *Lutaml::Model::Serializable* to `LiquidDrop` to be safely used in liquid templates. The returned drop provides all the attributes defined in the class as methods.
4612
+ * All attributes are accessible in the Liquid template by their names.
4613
+ * Nested attributes are also converted into `Liquid::Drop` objects so
4614
+ inner attributes can be accessed using the Liquid dot notation.
4615
+
4616
+ NOTE: Every `Lutaml::Model::Serializable` class extends the `Liquefiable` module
4617
+ which generates a corresponding `Liquid::Drop` class.
4618
+
4619
+ NOTE: Methods defined in the `Lutaml::Model::Serializable` class are not
4620
+ accessible in the Liquid template.
4621
+
4622
+ .Using `to_liquid` to convert model instances into corresponding Liquid drop instances
4601
4623
 
4602
4624
  [example]
4603
4625
  ====
4604
4626
  [source,ruby]
4605
4627
  ----
4606
- class Person < Lutaml::Model::Serializable
4628
+ class Ceramic < Lutaml::Model::Serializable
4629
+ attribute :name, :string
4630
+ attribute :temperature, :integer
4631
+ end
4632
+
4633
+ ceramic = Ceramic.new({ name: "Porcelain Vase", temperature: 1200 })
4634
+ ceramic_drop = ceramic.to_liquid
4635
+ # Ceramic::CeramicDrop
4636
+
4637
+ puts ceramic_drop.name
4638
+ # "Porcelain Vase"
4639
+ puts ceramic_drop.temperature
4640
+ # 1200
4641
+ ----
4642
+ ====
4643
+
4644
+ .Accessing LutaML::Model objects within a Liquid template
4645
+ [example]
4646
+ ====
4647
+ [source,ruby]
4648
+ ----
4649
+ class Ceramic < Lutaml::Model::Serializable
4650
+ attribute :name, :string
4651
+ attribute :temperature, :integer
4652
+ end
4653
+
4654
+ class CeramicCollection < Lutaml::Model::Serializable
4655
+ attribute :ceramics, Ceramic, collection: true
4656
+ end
4657
+ ----
4658
+
4659
+ `sample.yml`:
4660
+
4661
+ [source,yaml]
4662
+ ----
4663
+ ---
4664
+ ceramics:
4665
+ - name: Porcelain Vase
4666
+ temperature: 1200
4667
+ - name: Earthenware Pot
4668
+ temperature: 950
4669
+ - name: Stoneware Jug
4670
+ temperature: 1200
4671
+ ----
4672
+
4673
+ `template.liquid`:
4674
+
4675
+ [source,liquid]
4676
+ ----
4677
+ {% for ceramic in ceramic_collection.ceramics %}
4678
+ * Name: "{{ ceramic.name }}"
4679
+ ** Temperature: {{ ceramic.temperature }}
4680
+ {%- endfor %}
4681
+ ----
4682
+
4683
+ [source,ruby]
4684
+ ----
4685
+ # Load the Lutaml::Model collection
4686
+ ceramic_collection = CeramicCollection.from_yaml(File.read("sample.yml"))
4687
+
4688
+ # Load the Liquid template
4689
+ template = Liquid::Template.parse(File.read("template.liquid"))
4690
+
4691
+ # Pass the Lutaml::Model collection to the Liquid template and render
4692
+ output = template.render("ceramic_collection" => ceramic_collection)
4693
+ puts output
4694
+ # >
4695
+ # * Name: "Porcelain Vase"
4696
+ # ** Temperature: 1200
4697
+ # * Name: "Earthenware Pot"
4698
+ # ** Temperature: 950
4699
+ # * Name: "Stoneware Jug"
4700
+ # ** Temperature: 1200
4701
+ ----
4702
+ ====
4703
+
4704
+ .Accessing nested LutaML::Model objects within nested Liquid templates
4705
+ [example]
4706
+ ====
4707
+ [source,ruby]
4708
+ ----
4709
+ class Glaze < Lutaml::Model::Serializable
4710
+ attribute :color, :string
4711
+ attribute :opacity, :string
4712
+ end
4713
+
4714
+ class CeramicWork < Lutaml::Model::Serializable
4607
4715
  attribute :name, :string
4608
- attribute :age, integer
4716
+ attribute :glaze, Glaze
4717
+ end
4718
+
4719
+ class CeramicCollection < Lutaml::Model::Serializable
4720
+ attribute :ceramics, Ceramic, collection: true
4609
4721
  end
4610
4722
 
4611
- person = Person.new({ name: "John", age: 22 })
4612
- person_drop = person.to_liquid
4613
- # Person::PersonDrop
4723
+ ceramic_work = CeramicWork.new({
4724
+ name: "Celadon Bowl",
4725
+ glaze: Glaze.new({
4726
+ color: "Jade Green",
4727
+ opacity: "Translucent"
4728
+ })
4729
+ })
4730
+ ceramic_work_drop = ceramic_work.to_liquid
4731
+ # CeramicWork::CeramicWorkDrop
4732
+
4733
+ puts ceramic_work_drop.name
4734
+ # "Celadon Bowl"
4735
+ puts ceramic_work_drop.glaze.color
4736
+ # "Jade Green"
4737
+ puts ceramic_work_drop.glaze.opacity
4738
+ # "Translucent"
4739
+ ----
4740
+
4741
+ `ceramics.yml`:
4742
+
4743
+ [source,yaml]
4744
+ ----
4745
+ ---
4746
+ ceramics:
4747
+ - name: Celadon Bowl
4748
+ glaze:
4749
+ color: Jade Green
4750
+ opacity: Translucent
4751
+ - name: Earthenware Pot
4752
+ glaze:
4753
+ color: Rust Red
4754
+ opacity: Opaque
4755
+ - name: Stoneware Jug
4756
+ glaze:
4757
+ color: Cobalt Blue
4758
+ opacity: Transparent
4759
+ ----
4760
+
4761
+
4762
+ `templates/_ceramics.liquid`:
4763
+
4764
+ [source,liquid]
4765
+ ----
4766
+ {% for ceramic in ceramic_collection.ceramics %}
4767
+ {% render 'ceramic' ceramic: ceramic %}
4768
+ {%- endfor %}
4769
+ ----
4770
+
4771
+ NOTE: `render` is a Liquid tag that renders a partial template, by default
4772
+ Liquid uses the pattern `_%s.liquid` to find the partial template. Here
4773
+ `ceramic` refers to the file at `templates/_ceramic.liquid`.
4774
+
4775
+ `templates/_ceramic.liquid`:
4776
+
4777
+ [source,liquid]
4778
+ ----
4779
+ * Name: "{{ ceramic.name }}"
4780
+ ** Temperature: {{ ceramic.temperature }}
4781
+ {%- if ceramic.glaze %}
4782
+ ** Glaze (color): {{ ceramic.glaze.color }}
4783
+ ** Glaze (opacity): {{ ceramic.glaze.opacity }}
4784
+ {%- endif %}
4785
+ ----
4786
+
4787
+ [source,ruby]
4788
+ ----
4789
+ require 'liquid'
4790
+
4791
+ # Create a Liquid template object that supports dynamic loading
4792
+ template = Liquid::Template.new
4793
+
4794
+ # Link the Liquid template object to a "local file system" (directory)
4795
+ file_system = Liquid::LocalFileSystem.new('templates/')
4796
+ template.registers[:file_system] = file_system
4797
+
4798
+ # Load the partial template, this is necessary.
4799
+ # This will also allow Liquid to load any inner partials from the file system
4800
+ # dynamically (see `file_system.pattern` to see what it loads)
4801
+ template.parse(file_system.read_template_file('ceramics'))
4802
+
4803
+ # Read the lutaml-model collection
4804
+ ceramic_collection = CeramicCollection.from_yaml(File.read("ceramics.yml"))
4614
4805
 
4615
- puts person_drop.name
4616
- # "John"
4617
- puts person_drop.age
4618
- # 22
4806
+ # Render the template with the collection
4807
+ output = template.render("ceramic_collection" => ceramic_collection)
4808
+ puts output
4809
+ # >
4810
+ # * Name: "Celadon Bowl"
4811
+ # ** Temperature: 1200
4812
+ # ** Glaze (color): Jade Green
4813
+ # ** Glaze (finish): Translucent
4814
+ # * Name: "Earthenware Pot"
4815
+ # ** Temperature: 950
4816
+ # ** Glaze (color): Rust Red
4817
+ # ** Glaze (finish): Opaque
4818
+ # * Name: "Stoneware Jug"
4819
+ # ** Temperature: 1200
4820
+ # ** Glaze (color): Cobalt Blue
4821
+ # ** Glaze (finish): Transparent
4619
4822
  ----
4620
4823
  ====
4621
4824
 
@@ -0,0 +1,9 @@
1
+ module Lutaml
2
+ module Model
3
+ class LiquidNotEnabledError < Error
4
+ def to_s
5
+ "Liquid functionality is not available by default; please install and require `liquid` gem to use this functionality"
6
+ end
7
+ end
8
+ end
9
+ end
@@ -6,6 +6,7 @@ module Lutaml
6
6
  end
7
7
 
8
8
  require_relative "error/invalid_value_error"
9
+ require_relative "error/liquid_not_enabled_error"
9
10
  require_relative "error/incorrect_mapping_argument_error"
10
11
  require_relative "error/pattern_not_matched_error"
11
12
  require_relative "error/unknown_adapter_type_error"
@@ -1,5 +1,3 @@
1
- require "liquid"
2
-
3
1
  module Lutaml
4
2
  module Model
5
3
  module Liquefiable
@@ -9,6 +7,7 @@ module Lutaml
9
7
 
10
8
  module ClassMethods
11
9
  def register_liquid_drop_class
10
+ validate_liquid!
12
11
  if drop_class
13
12
  raise "#{drop_class_name} Already exists!"
14
13
  end
@@ -38,6 +37,7 @@ module Lutaml
38
37
 
39
38
  def register_drop_method(method_name)
40
39
  register_liquid_drop_class unless drop_class
40
+ return if drop_class.method_defined?(method_name)
41
41
 
42
42
  drop_class.define_method(method_name) do
43
43
  value = @object.public_send(method_name)
@@ -49,9 +49,22 @@ module Lutaml
49
49
  end
50
50
  end
51
51
  end
52
+
53
+ def validate_liquid!
54
+ return if Object.const_defined?(:Liquid)
55
+
56
+ raise Lutaml::Model::LiquidNotEnabledError
57
+ end
52
58
  end
53
59
 
54
60
  def to_liquid
61
+ self.class.validate_liquid!
62
+
63
+ if is_a?(Lutaml::Model::Serializable)
64
+ self.class.attributes.each_key do |attr_name|
65
+ self.class.register_drop_method(attr_name)
66
+ end
67
+ end
55
68
  self.class.drop_class.new(self)
56
69
  end
57
70
  end
@@ -117,8 +117,6 @@ module Lutaml
117
117
  end
118
118
  end
119
119
 
120
- register_drop_method(name)
121
-
122
120
  attr
123
121
  end
124
122
 
@@ -6,13 +6,14 @@ module Lutaml
6
6
  self.class.attributes.each do |name, attr|
7
7
  value = public_send(:"#{name}")
8
8
  begin
9
- if value.respond_to?(:validate!)
10
- value.validate!
9
+ if value.respond_to?(:validate)
10
+ errors.concat(value.validate)
11
11
  else
12
12
  attr.validate_value!(value)
13
13
  end
14
14
  rescue Lutaml::Model::InvalidValueError,
15
15
  Lutaml::Model::CollectionCountOutOfRangeError,
16
+ Lutaml::Model::CollectionTrueMissingError,
16
17
  PatternNotMatchedError => e
17
18
  errors << e
18
19
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Lutaml
4
4
  module Model
5
- VERSION = "0.6.2"
5
+ VERSION = "0.6.4"
6
6
  end
7
7
  end
@@ -0,0 +1,6 @@
1
+ * Name: "{{ ceramic.name }}"
2
+ ** Temperature: {{ ceramic.temperature }}
3
+ {%- if ceramic.glaze %}
4
+ ** Glaze (color): {{ ceramic.glaze.color }}
5
+ ** Glaze (opacity): {{ ceramic.glaze.opacity }}
6
+ {%- endif %}
@@ -0,0 +1,3 @@
1
+ {% for ceramic in ceramic_collection.ceramics %}
2
+ {% render 'ceramic' ceramic: ceramic %}
3
+ {%- endfor %}
@@ -0,0 +1,4 @@
1
+ {% for ceramic in ceramic_collection.ceramics %}
2
+ * Name: "{{ ceramic.name }}"
3
+ ** Temperature: {{ ceramic.temperature }}
4
+ {%- endfor %}
@@ -1,4 +1,5 @@
1
1
  require "spec_helper"
2
+ require "liquid"
2
3
  require_relative "../../fixtures/address"
3
4
 
4
5
  class LiquefiableClass
@@ -16,6 +17,23 @@ class LiquefiableClass
16
17
  end
17
18
  end
18
19
 
20
+ module LiquefiableSpec
21
+ class Glaze < Lutaml::Model::Serializable
22
+ attribute :color, :string
23
+ attribute :opacity, :string
24
+ end
25
+
26
+ class Ceramic < Lutaml::Model::Serializable
27
+ attribute :name, :string
28
+ attribute :temperature, :integer
29
+ attribute :glaze, Glaze
30
+ end
31
+
32
+ class CeramicCollection < Lutaml::Model::Serializable
33
+ attribute :ceramics, Ceramic, collection: true
34
+ end
35
+ end
36
+
19
37
  RSpec.describe Lutaml::Model::Liquefiable do
20
38
  before do
21
39
  stub_const("DummyModel", Class.new(LiquefiableClass))
@@ -26,14 +44,27 @@ RSpec.describe Lutaml::Model::Liquefiable do
26
44
  describe ".register_liquid_drop_class" do
27
45
  context "when drop class does not exist" do
28
46
  it "creates a new drop class" do
29
- expect { dummy.class.register_liquid_drop_class }.to change {
30
- dummy.class.const_defined?(:DummyModelDrop)
31
- }
47
+ expect do
48
+ dummy.class.register_liquid_drop_class
49
+ end.to change {
50
+ dummy.class.const_defined?(:DummyModelDrop)
51
+ }
32
52
  .from(false)
33
53
  .to(true)
34
54
  end
35
55
  end
36
56
 
57
+ context "when 'liquid' is not available" do
58
+ before { allow(Object).to receive(:const_defined?).with(:Liquid).and_return(false) }
59
+
60
+ it "raises an error" do
61
+ expect { dummy.class.register_liquid_drop_class }.to raise_error(
62
+ Lutaml::Model::LiquidNotEnabledError,
63
+ "Liquid functionality is not available by default; please install and require `liquid` gem to use this functionality",
64
+ )
65
+ end
66
+ end
67
+
37
68
  context "when drop class already exists" do
38
69
  it "raises an error" do
39
70
  dummy.class.register_liquid_drop_class
@@ -69,26 +100,41 @@ RSpec.describe Lutaml::Model::Liquefiable do
69
100
  end
70
101
 
71
102
  it "defines a method on the drop class" do
72
- expect { dummy.class.register_drop_method(:display_name) }.to change {
73
- dummy.to_liquid.respond_to?(:display_name)
74
- }
103
+ expect do
104
+ dummy.class.register_drop_method(:display_name)
105
+ end.to change {
106
+ dummy.to_liquid.respond_to?(:display_name)
107
+ }
75
108
  .from(false)
76
109
  .to(true)
77
110
  end
78
111
  end
79
112
 
80
113
  describe ".to_liquid" do
81
- before do
82
- dummy.class.register_liquid_drop_class
83
- dummy.class.register_drop_method(:display_name)
84
- end
114
+ context "when liquid is not enabled" do
115
+ before { allow(Object).to receive(:const_defined?).with(:Liquid).and_return(false) }
85
116
 
86
- it "returns an instance of the drop class" do
87
- expect(dummy.to_liquid).to be_a(dummy.class.drop_class)
117
+ it "raises an error" do
118
+ expect { dummy.to_liquid }.to raise_error(
119
+ Lutaml::Model::LiquidNotEnabledError,
120
+ "Liquid functionality is not available by default; please install and require `liquid` gem to use this functionality",
121
+ )
122
+ end
88
123
  end
89
124
 
90
- it "allows access to registered methods via the drop class" do
91
- expect(dummy.to_liquid.display_name).to eq("TestName (42)")
125
+ context "when liquid is enabled" do
126
+ before do
127
+ dummy.class.register_liquid_drop_class
128
+ dummy.class.register_drop_method(:display_name)
129
+ end
130
+
131
+ it "returns an instance of the drop class" do
132
+ expect(dummy.to_liquid).to be_a(dummy.class.drop_class)
133
+ end
134
+
135
+ it "allows access to registered methods via the drop class" do
136
+ expect(dummy.to_liquid.display_name).to eq("TestName (42)")
137
+ end
92
138
  end
93
139
  end
94
140
 
@@ -118,4 +164,96 @@ RSpec.describe Lutaml::Model::Liquefiable do
118
164
  end
119
165
  end
120
166
  end
167
+
168
+ describe "working with liquid templates" do
169
+ let(:liquid_template_dir) do
170
+ File.join(File.dirname(__FILE__), "../../fixtures/liquid_templates")
171
+ end
172
+
173
+ describe "rendering simple models with liquid templates" do
174
+ let :yaml do
175
+ <<~YAML
176
+ ---
177
+ ceramics:
178
+ - name: Porcelain Vase
179
+ temperature: 1200
180
+ - name: Earthenware Pot
181
+ temperature: 950
182
+ - name: Stoneware Jug
183
+ temperature: 1200
184
+ YAML
185
+ end
186
+ let :template_path do
187
+ File.join(liquid_template_dir, "_ceramics_in_one.liquid")
188
+ end
189
+
190
+ it "renders" do
191
+ template = Liquid::Template.parse(File.read(template_path))
192
+ ceramic_collection = LiquefiableSpec::CeramicCollection.from_yaml(yaml)
193
+ output = template.render("ceramic_collection" => ceramic_collection)
194
+
195
+ expected_output = <<~OUTPUT
196
+ * Name: "Porcelain Vase"
197
+ ** Temperature: 1200
198
+ * Name: "Earthenware Pot"
199
+ ** Temperature: 950
200
+ * Name: "Stoneware Jug"
201
+ ** Temperature: 1200
202
+ OUTPUT
203
+
204
+ expect(output.strip).to eq(expected_output.strip)
205
+ end
206
+ end
207
+
208
+ describe "rendering nested models with liquid templates from file system" do
209
+ let :yaml do
210
+ <<~YAML
211
+ ---
212
+ ceramics:
213
+ - name: Celadon Bowl
214
+ temperature: 1200
215
+ glaze:
216
+ color: Jade Green
217
+ opacity: Translucent
218
+ - name: Earthenware Pot
219
+ temperature: 950
220
+ glaze:
221
+ color: Rust Red
222
+ opacity: Opaque
223
+ - name: Stoneware Jug
224
+ temperature: 1200
225
+ glaze:
226
+ color: Cobalt Blue
227
+ opacity: Transparent
228
+ YAML
229
+ end
230
+
231
+ it "renders" do
232
+ template = Liquid::Template.new
233
+ file_system = Liquid::LocalFileSystem.new(liquid_template_dir)
234
+ template.registers[:file_system] = file_system
235
+ template.parse(file_system.read_template_file("ceramics"))
236
+
237
+ ceramic_collection = LiquefiableSpec::CeramicCollection.from_yaml(yaml)
238
+ output = template.render("ceramic_collection" => ceramic_collection)
239
+ # puts output
240
+
241
+ expected_output = <<~OUTPUT
242
+ * Name: "Celadon Bowl"
243
+ ** Temperature: 1200
244
+ ** Glaze (color): Jade Green
245
+ ** Glaze (opacity): Translucent
246
+ * Name: "Earthenware Pot"
247
+ ** Temperature: 950
248
+ ** Glaze (color): Rust Red
249
+ ** Glaze (opacity): Opaque
250
+ * Name: "Stoneware Jug"
251
+ ** Temperature: 1200
252
+ ** Glaze (color): Cobalt Blue
253
+ ** Glaze (opacity): Transparent
254
+ OUTPUT
255
+ expect(output.strip).to eq(expected_output.strip)
256
+ end
257
+ end
258
+ end
121
259
  end
@@ -12,6 +12,18 @@ RSpec.describe Lutaml::Model::Validation do
12
12
  attribute :tags, :string, collection: true
13
13
  attribute :role, :string, collection: 1..3
14
14
  end)
15
+
16
+ stub_const("ValidationTestMainClass", Class.new(Lutaml::Model::Serializable) do
17
+ attribute :test_class, ValidationTestClass
18
+
19
+ xml do
20
+ map_element "test_class", to: :test_class
21
+ end
22
+
23
+ key_value do
24
+ map "test_class", to: :test_class
25
+ end
26
+ end)
15
27
  end
16
28
 
17
29
  let(:valid_instance) do
@@ -24,15 +36,29 @@ RSpec.describe Lutaml::Model::Validation do
24
36
  )
25
37
  end
26
38
 
39
+ let(:valid_nested_instance) do
40
+ ValidationTestMainClass.new(
41
+ test_class: valid_instance,
42
+ )
43
+ end
44
+
27
45
  describe "#validate" do
28
46
  it "returns an empty array for a valid instance" do
29
47
  expect(valid_instance.validate).to be_empty
30
48
  end
31
49
 
32
- xit "returns errors for invalid integer value" do
50
+ it "returns errors for invalid integer value" do
33
51
  instance = ValidationTestClass.new(age: "thirty", role: ["admin"])
34
52
  errors = instance.validate
35
- expect(errors).to include("Invalid value for attribute age: thirty")
53
+ expect(errors).to eq([])
54
+ expect(instance.age).to be_nil
55
+ end
56
+
57
+ it "raises error if Array is set but collection is not set" do
58
+ instance = ValidationTestClass.new(name: ["admin"])
59
+ expect do
60
+ instance.validate
61
+ end.not_to raise_error(Lutaml::Model::CollectionTrueMissingError)
36
62
  end
37
63
 
38
64
  it "returns errors for value not in allowed set" do
@@ -55,17 +81,20 @@ RSpec.describe Lutaml::Model::Validation do
55
81
  end
56
82
  end
57
83
 
58
- xit "returns multiple errors for multiple invalid attributes" do
84
+ it "returns multiple errors for multiple invalid attributes" do
59
85
  instance = ValidationTestClass.new(name: "123", age: "thirty",
60
86
  email: "invalid@example.com", role: [])
61
87
  expect do
62
88
  instance.validate!
63
89
  end.to raise_error(Lutaml::Model::ValidationError) do |error|
64
- expect(error.error_messages.join("\n")).to include("Invalid value for attribute age: thirty")
65
90
  expect(error.error_messages.join("\n")).to include("email is `invalid@example.com`, must be one of the following [test@example.com, user@example.com]")
66
91
  expect(error.error_messages.join("\n")).to include("role count is 0, must be between 1 and 3")
67
92
  end
68
93
  end
94
+
95
+ it "returns an empty array for a valid nested instance" do
96
+ expect(valid_nested_instance.validate).to be_empty
97
+ end
69
98
  end
70
99
 
71
100
  describe "#validate!" do
@@ -73,13 +102,31 @@ RSpec.describe Lutaml::Model::Validation do
73
102
  expect { valid_instance.validate! }.not_to raise_error
74
103
  end
75
104
 
76
- xit "raises a ValidationError with all error messages for an invalid instance" do
105
+ it "raises a ValidationError with all error messages for an invalid instance" do
77
106
  instance = ValidationTestClass.new(name: "test", age: "thirty")
78
107
  expect do
79
108
  instance.validate!
80
109
  end.to raise_error(Lutaml::Model::ValidationError) do |error|
81
110
  expect(error.error_messages.join("\n")).to include("role count is 0, must be between 1 and 3")
82
- expect(error.error_messages.join("\n")).to include("Invalid value for attribute age: thirty")
111
+ end
112
+ end
113
+
114
+ it "validates nested ValidationTestClass instance" do
115
+ invalid_nested = ValidationTestMainClass.new(
116
+ test_class: ValidationTestClass.new(
117
+ name: "John Doe",
118
+ age: 30,
119
+ email: "invalid@example.com",
120
+ role: ["admin"],
121
+ ),
122
+ )
123
+
124
+ expect do
125
+ invalid_nested.validate!
126
+ end.to raise_error(Lutaml::Model::ValidationError) do |error|
127
+ expect(error.error_messages.join("\n")).to include(
128
+ "email is `invalid@example.com`, must be one of the following [test@example.com, user@example.com]",
129
+ )
83
130
  end
84
131
  end
85
132
  end
data/spec/spec_helper.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "liquid"
3
4
  require "rspec/matchers"
4
5
  require "equivalent-xml"
5
6
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lutaml-model
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.2
4
+ version: 0.6.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-02-13 00:00:00.000000000 Z
11
+ date: 2025-02-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: base64
@@ -111,6 +111,7 @@ files:
111
111
  - lib/lutaml/model/error/incorrect_sequence_error.rb
112
112
  - lib/lutaml/model/error/invalid_choice_range_error.rb
113
113
  - lib/lutaml/model/error/invalid_value_error.rb
114
+ - lib/lutaml/model/error/liquid_not_enabled_error.rb
114
115
  - lib/lutaml/model/error/multiple_mappings_error.rb
115
116
  - lib/lutaml/model/error/no_root_mapping_error.rb
116
117
  - lib/lutaml/model/error/no_root_namespace_error.rb
@@ -192,6 +193,9 @@ files:
192
193
  - spec/ceramic_spec.rb
193
194
  - spec/fixtures/address.rb
194
195
  - spec/fixtures/ceramic.rb
196
+ - spec/fixtures/liquid_templates/_ceramic.liquid
197
+ - spec/fixtures/liquid_templates/_ceramics.liquid
198
+ - spec/fixtures/liquid_templates/_ceramics_in_one.liquid
195
199
  - spec/fixtures/person.rb
196
200
  - spec/fixtures/sample_model.rb
197
201
  - spec/fixtures/vase.rb