lutaml-model 0.6.3 → 0.6.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 99e9857c1f8a86d0fb3566c6d9e5e11723e203e7f58bd7fcfd3c5a0cb1702b6c
4
- data.tar.gz: 270002530a82256d2682a060a9616320b76f9592aeb74d17451d26d311a45508
3
+ metadata.gz: 07d4f2ac62702a8ac27070d4d3246e59c367937cf50b649979ecb3c525f9b60e
4
+ data.tar.gz: 31adf8c870b4196ea69a065559ebbb5c2f35719aba3256597d9e24474bcbfa57
5
5
  SHA512:
6
- metadata.gz: f1d98496343787aad60a34ee00c7df537b9fcd4160209cdd920d8265afdc048e7a26dbc3567b30ed1a5f5eb8120684b81dc9bb04a8964fd4495be67ae339bf5e
7
- data.tar.gz: d51b92c1bda980ac8f30890236329880db515eb11e20e73a647dc78cdba2de58fc869d58a5905a20681f4b946dff205968f9f1cf619bac10aa677157497f05ff
6
+ metadata.gz: 5ef7e89a66c74f214202d44a5b8a55d7c2aedde5129e2723f36128e8651a019cd578df6d24c375b2f760f81cd9dcd2d85ed6ae5f8972345467c13466da71c527
7
+ data.tar.gz: 74aff7dee7711d4c9ec88730332b91f15ba530b92946b50b1cb218157856c1c31e3c6d9bdf292b8d9ab2832586c2ee61ae29d49a8254fecbd44cc92510f6667e
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-21 10:20:24 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
@@ -27,7 +27,7 @@ Layout/IndentationWidth:
27
27
  Exclude:
28
28
  - 'lib/lutaml/model/schema/xml_compiler.rb'
29
29
 
30
- # Offense count: 467
30
+ # Offense count: 466
31
31
  # This cop supports safe autocorrection (--autocorrect).
32
32
  # Configuration parameters: Max, AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, AllowedPatterns, SplitStrings.
33
33
  # URISchemes: http, https
@@ -68,7 +68,7 @@ Metrics/AbcSize:
68
68
  Metrics/BlockLength:
69
69
  Max: 46
70
70
 
71
- # Offense count: 48
71
+ # Offense count: 47
72
72
  # Configuration parameters: AllowedMethods, AllowedPatterns, Max.
73
73
  Metrics/CyclomaticComplexity:
74
74
  Exclude:
@@ -85,7 +85,7 @@ Metrics/CyclomaticComplexity:
85
85
  - 'lib/lutaml/model/xml_adapter/ox_adapter.rb'
86
86
  - 'lib/lutaml/model/xml_adapter/xml_document.rb'
87
87
 
88
- # Offense count: 87
88
+ # Offense count: 86
89
89
  # Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns.
90
90
  Metrics/MethodLength:
91
91
  Max: 45
@@ -95,7 +95,7 @@ Metrics/MethodLength:
95
95
  Metrics/ParameterLists:
96
96
  Max: 15
97
97
 
98
- # Offense count: 37
98
+ # Offense count: 36
99
99
  # Configuration parameters: AllowedMethods, AllowedPatterns, Max.
100
100
  Metrics/PerceivedComplexity:
101
101
  Exclude:
@@ -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
@@ -347,6 +347,44 @@ same class and all their attributes are equal.
347
347
 
348
348
  == Defining attributes
349
349
 
350
+ === Derived Attributes
351
+
352
+ A derived attribute is computed dynamically based on an instance method instead of storing a static value. It is defined using the `method:` option.
353
+
354
+ Syntax:
355
+
356
+ [source,ruby]
357
+ ----
358
+ attribute :name_of_attribute, method: :instance_method_name
359
+ ----
360
+
361
+ .Defining methods as attributes
362
+ [example]
363
+ ====
364
+ [source,ruby]
365
+ ----
366
+ class Invoice < Lutaml::Model::Serializable
367
+ attribute :subtotal, :float
368
+ attribute :tax, :float
369
+ attribute :total, method: :total_value
370
+
371
+ def total_value
372
+ subtotal + tax
373
+ end
374
+ end
375
+
376
+ i = Invoice.new(subtotal: 100.0, tax: 12.0)
377
+ i.total
378
+ #=> 112.0
379
+
380
+ puts i.to_yaml
381
+ #=> ---
382
+ #=> subtotal: 100.0
383
+ #=> tax: 12.0
384
+ #=> total: 112.0
385
+ ----
386
+ ====
387
+
350
388
  === Supported attribute value types
351
389
 
352
390
  ==== General types
@@ -413,9 +451,11 @@ end
413
451
 
414
452
  ==== Decimal type
415
453
 
416
- The Decimal type is an optional type that is disabled by default.
454
+ WARNING: Decimal is an optional feature.
417
455
 
418
- NOTE: The reason why the Decimal type is disalbed by default is that the
456
+ The Decimal type is a value type that is disabled by default.
457
+
458
+ NOTE: The reason why the Decimal type is disabled by default is that the
419
459
  `BigDecimal` class became optional to the standard Ruby library from Ruby 3.4
420
460
  onwards. The `Decimal` type is only enabled when the `bigdecimal` library is
421
461
  loaded.
@@ -693,7 +733,7 @@ end
693
733
  Lutaml lets you create reusable element and attribute collections using `no_root`. These can be imported into other models using:
694
734
 
695
735
  - `import_model`: imports both attributes and mappings
696
- - `import_model_attributes`: imports only attributes
736
+ - `import_model_attributes`: imports only attributes
697
737
  - `import_model_mappings`: imports only mappings
698
738
 
699
739
  NOTE: This feature works with XML. Import order determines how elements and attributes are overwritten.
@@ -3898,7 +3938,7 @@ class Person < Lutaml::Model::Serializable
3898
3938
  }
3899
3939
  end
3900
3940
 
3901
- # Mapping-level transformation in XML format
3941
+ # Mapping-level transformation in XML format
3902
3942
  xml do
3903
3943
  map "full-name", to: :name, transform: {
3904
3944
  export: ->(value) { "Dr. #{value}" },
@@ -4098,39 +4138,39 @@ NOTE: For `NokogiriAdapter`, we can also call `to_xml` on `value.node.adapter_no
4098
4138
 
4099
4139
  # Nokogiri Adapter Node
4100
4140
 
4101
- #<Lutaml::Model::XmlAdapter::NokogiriElement:0x0000000107656ed8
4102
- # @attributes={},
4103
- # @children=
4141
+ #<Lutaml::Model::XmlAdapter::NokogiriElement:0x0000000107656ed8
4142
+ # @attributes={},
4143
+ # @children=
4104
4144
  # [#<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=
4145
+ # #<Lutaml::Model::XmlAdapter::NokogiriElement:0x00000001076569b0
4146
+ # @attributes={},
4147
+ # @children=
4108
4148
  # [#<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">,
4149
+ # @default_namespace=nil,
4150
+ # @name="category",
4151
+ # @namespace_prefix=nil,
4152
+ # @text="Metadata">,
4113
4153
  # #<Lutaml::Model::XmlAdapter::NokogiriElement:0x0000000107656028 @attributes={}, @children=[], @default_namespace=nil, @name="text", @namespace_prefix=nil, @text="\n ">],
4114
- # @default_namespace=nil,
4154
+ # @default_namespace=nil,
4115
4155
  # @name="metadata",
4116
4156
  # @namespace_prefix=nil,
4117
4157
  # @text="\n Metadata\n ">
4118
4158
 
4119
4159
  # Ox Adapter Node
4120
4160
 
4121
- #<Lutaml::Model::XmlAdapter::OxElement:0x0000000107584f78
4122
- # @attributes={},
4123
- # @children=
4124
- # [#<Lutaml::Model::XmlAdapter::OxElement:0x0000000107584e60
4125
- # @attributes={},
4161
+ #<Lutaml::Model::XmlAdapter::OxElement:0x0000000107584f78
4162
+ # @attributes={},
4163
+ # @children=
4164
+ # [#<Lutaml::Model::XmlAdapter::OxElement:0x0000000107584e60
4165
+ # @attributes={},
4126
4166
  # @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,
4167
+ # @default_namespace=nil,
4168
+ # @name="category",
4169
+ # @namespace_prefix=nil,
4170
+ # @text="Metadata">],
4171
+ # @default_namespace=nil,
4172
+ # @name="metadata",
4173
+ # @namespace_prefix=nil,
4134
4174
  # @text=nil>
4135
4175
 
4136
4176
  # Oga Adapter Node
@@ -4190,7 +4230,7 @@ class CustomModelParentMapper < Lutaml::Model::Serializable
4190
4230
  map_element :CustomModelChild,
4191
4231
  with: { to: :child_to_xml, from: :child_from_xml }
4192
4232
  end
4193
-
4233
+
4194
4234
  def child_to_xml(model, parent, doc)
4195
4235
  child_el = doc.create_element("CustomModelChild")
4196
4236
  street_el = doc.create_element("street")
@@ -4227,7 +4267,7 @@ end
4227
4267
  [source,ruby]
4228
4268
  ----
4229
4269
  > instance = CustomModelParentMapper.from_xml(xml)
4230
- > #<CustomModelParent:0x0000000107c9ca68 @child_mapper=#<CustomModelChild:0x0000000107c95218 @city="London", @street="Oxford Street">, @first_name="John">
4270
+ > #<CustomModelParent:0x0000000107c9ca68 @child_mapper=#<CustomModelChild:0x0000000107c95218 @city="London", @street="Oxford Street">, @first_name="John">
4231
4271
  > CustomModelParentMapper.to_xml(instance)
4232
4272
  > #<CustomModelParent><first_name>John</first_name><CustomModelChild><street>Oxford Street</street><city>London</city></CustomModelChild></CustomModelParent>
4233
4273
  ----
@@ -4595,27 +4635,228 @@ klin.validate
4595
4635
  ====
4596
4636
 
4597
4637
 
4598
- == Liquid Compatability
4638
+ == Liquid template access
4639
+
4640
+ WARNING: The Liquid template feature is optional. To enable it, please
4641
+ explicitly require the `liquid` gem.
4642
+
4643
+ The https://shopify.github.io/liquid/[Liquid template language] is an
4644
+ open-source template language developed by Shopify and written in Ruby.
4599
4645
 
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.
4646
+ `Lutaml::Model::Serializable` objects can be safely accessed within Liquid
4647
+ templates through a `to_liquid` method that converts the objects into
4648
+ `Liquid::Drop` instances.
4649
+
4650
+ * All attributes are accessible in the Liquid template by their names.
4651
+ * Nested attributes are also converted into `Liquid::Drop` objects so
4652
+ inner attributes can be accessed using the Liquid dot notation.
4653
+
4654
+ NOTE: Every `Lutaml::Model::Serializable` class extends the `Liquefiable` module
4655
+ which generates a corresponding `Liquid::Drop` class.
4656
+
4657
+ NOTE: Methods defined in the `Lutaml::Model::Serializable` class are not
4658
+ accessible in the Liquid template.
4659
+
4660
+ .Using `to_liquid` to convert model instances into corresponding Liquid drop instances
4601
4661
 
4602
4662
  [example]
4603
4663
  ====
4604
4664
  [source,ruby]
4605
4665
  ----
4606
- class Person < Lutaml::Model::Serializable
4666
+ class Ceramic < Lutaml::Model::Serializable
4667
+ attribute :name, :string
4668
+ attribute :temperature, :integer
4669
+ end
4670
+
4671
+ ceramic = Ceramic.new({ name: "Porcelain Vase", temperature: 1200 })
4672
+ ceramic_drop = ceramic.to_liquid
4673
+ # Ceramic::CeramicDrop
4674
+
4675
+ puts ceramic_drop.name
4676
+ # "Porcelain Vase"
4677
+ puts ceramic_drop.temperature
4678
+ # 1200
4679
+ ----
4680
+ ====
4681
+
4682
+ .Accessing LutaML::Model objects within a Liquid template
4683
+ [example]
4684
+ ====
4685
+ [source,ruby]
4686
+ ----
4687
+ class Ceramic < Lutaml::Model::Serializable
4688
+ attribute :name, :string
4689
+ attribute :temperature, :integer
4690
+ end
4691
+
4692
+ class CeramicCollection < Lutaml::Model::Serializable
4693
+ attribute :ceramics, Ceramic, collection: true
4694
+ end
4695
+ ----
4696
+
4697
+ `sample.yml`:
4698
+
4699
+ [source,yaml]
4700
+ ----
4701
+ ---
4702
+ ceramics:
4703
+ - name: Porcelain Vase
4704
+ temperature: 1200
4705
+ - name: Earthenware Pot
4706
+ temperature: 950
4707
+ - name: Stoneware Jug
4708
+ temperature: 1200
4709
+ ----
4710
+
4711
+ `template.liquid`:
4712
+
4713
+ [source,liquid]
4714
+ ----
4715
+ {% for ceramic in ceramic_collection.ceramics %}
4716
+ * Name: "{{ ceramic.name }}"
4717
+ ** Temperature: {{ ceramic.temperature }}
4718
+ {%- endfor %}
4719
+ ----
4720
+
4721
+ [source,ruby]
4722
+ ----
4723
+ # Load the Lutaml::Model collection
4724
+ ceramic_collection = CeramicCollection.from_yaml(File.read("sample.yml"))
4725
+
4726
+ # Load the Liquid template
4727
+ template = Liquid::Template.parse(File.read("template.liquid"))
4728
+
4729
+ # Pass the Lutaml::Model collection to the Liquid template and render
4730
+ output = template.render("ceramic_collection" => ceramic_collection)
4731
+ puts output
4732
+ # >
4733
+ # * Name: "Porcelain Vase"
4734
+ # ** Temperature: 1200
4735
+ # * Name: "Earthenware Pot"
4736
+ # ** Temperature: 950
4737
+ # * Name: "Stoneware Jug"
4738
+ # ** Temperature: 1200
4739
+ ----
4740
+ ====
4741
+
4742
+ .Accessing nested LutaML::Model objects within nested Liquid templates
4743
+ [example]
4744
+ ====
4745
+ [source,ruby]
4746
+ ----
4747
+ class Glaze < Lutaml::Model::Serializable
4748
+ attribute :color, :string
4749
+ attribute :opacity, :string
4750
+ end
4751
+
4752
+ class CeramicWork < Lutaml::Model::Serializable
4607
4753
  attribute :name, :string
4608
- attribute :age, integer
4754
+ attribute :glaze, Glaze
4755
+ end
4756
+
4757
+ class CeramicCollection < Lutaml::Model::Serializable
4758
+ attribute :ceramics, Ceramic, collection: true
4609
4759
  end
4610
4760
 
4611
- person = Person.new({ name: "John", age: 22 })
4612
- person_drop = person.to_liquid
4613
- # Person::PersonDrop
4761
+ ceramic_work = CeramicWork.new({
4762
+ name: "Celadon Bowl",
4763
+ glaze: Glaze.new({
4764
+ color: "Jade Green",
4765
+ opacity: "Translucent"
4766
+ })
4767
+ })
4768
+ ceramic_work_drop = ceramic_work.to_liquid
4769
+ # CeramicWork::CeramicWorkDrop
4770
+
4771
+ puts ceramic_work_drop.name
4772
+ # "Celadon Bowl"
4773
+ puts ceramic_work_drop.glaze.color
4774
+ # "Jade Green"
4775
+ puts ceramic_work_drop.glaze.opacity
4776
+ # "Translucent"
4777
+ ----
4778
+
4779
+ `ceramics.yml`:
4780
+
4781
+ [source,yaml]
4782
+ ----
4783
+ ---
4784
+ ceramics:
4785
+ - name: Celadon Bowl
4786
+ glaze:
4787
+ color: Jade Green
4788
+ opacity: Translucent
4789
+ - name: Earthenware Pot
4790
+ glaze:
4791
+ color: Rust Red
4792
+ opacity: Opaque
4793
+ - name: Stoneware Jug
4794
+ glaze:
4795
+ color: Cobalt Blue
4796
+ opacity: Transparent
4797
+ ----
4798
+
4799
+
4800
+ `templates/_ceramics.liquid`:
4801
+
4802
+ [source,liquid]
4803
+ ----
4804
+ {% for ceramic in ceramic_collection.ceramics %}
4805
+ {% render 'ceramic' ceramic: ceramic %}
4806
+ {%- endfor %}
4807
+ ----
4808
+
4809
+ NOTE: `render` is a Liquid tag that renders a partial template, by default
4810
+ Liquid uses the pattern `_%s.liquid` to find the partial template. Here
4811
+ `ceramic` refers to the file at `templates/_ceramic.liquid`.
4812
+
4813
+ `templates/_ceramic.liquid`:
4814
+
4815
+ [source,liquid]
4816
+ ----
4817
+ * Name: "{{ ceramic.name }}"
4818
+ ** Temperature: {{ ceramic.temperature }}
4819
+ {%- if ceramic.glaze %}
4820
+ ** Glaze (color): {{ ceramic.glaze.color }}
4821
+ ** Glaze (opacity): {{ ceramic.glaze.opacity }}
4822
+ {%- endif %}
4823
+ ----
4824
+
4825
+ [source,ruby]
4826
+ ----
4827
+ require 'liquid'
4828
+
4829
+ # Create a Liquid template object that supports dynamic loading
4830
+ template = Liquid::Template.new
4831
+
4832
+ # Link the Liquid template object to a "local file system" (directory)
4833
+ file_system = Liquid::LocalFileSystem.new('templates/')
4834
+ template.registers[:file_system] = file_system
4835
+
4836
+ # Load the partial template, this is necessary.
4837
+ # This will also allow Liquid to load any inner partials from the file system
4838
+ # dynamically (see `file_system.pattern` to see what it loads)
4839
+ template.parse(file_system.read_template_file('ceramics'))
4840
+
4841
+ # Read the lutaml-model collection
4842
+ ceramic_collection = CeramicCollection.from_yaml(File.read("ceramics.yml"))
4614
4843
 
4615
- puts person_drop.name
4616
- # "John"
4617
- puts person_drop.age
4618
- # 22
4844
+ # Render the template with the collection
4845
+ output = template.render("ceramic_collection" => ceramic_collection)
4846
+ puts output
4847
+ # >
4848
+ # * Name: "Celadon Bowl"
4849
+ # ** Temperature: 1200
4850
+ # ** Glaze (color): Jade Green
4851
+ # ** Glaze (finish): Translucent
4852
+ # * Name: "Earthenware Pot"
4853
+ # ** Temperature: 950
4854
+ # ** Glaze (color): Rust Red
4855
+ # ** Glaze (finish): Opaque
4856
+ # * Name: "Stoneware Jug"
4857
+ # ** Temperature: 1200
4858
+ # ** Glaze (color): Cobalt Blue
4859
+ # ** Glaze (finish): Transparent
4619
4860
  ----
4620
4861
  ====
4621
4862
 
@@ -13,23 +13,20 @@ module Lutaml
13
13
  transform
14
14
  choice
15
15
  sequence
16
+ method_name
16
17
  ].freeze
17
18
 
18
19
  def initialize(name, type, options = {})
19
20
  @name = name
20
-
21
- validate_type!(type)
22
- @type = cast_type!(type)
23
-
24
- validate_options!(options)
25
21
  @options = options
26
22
 
27
- @raw = !!options[:raw]
23
+ validate_presence!(type, options[:method_name])
24
+ process_type!(type) if type
25
+ process_options!
26
+ end
28
27
 
29
- if collection?
30
- validate_collection_range
31
- @options[:default] = -> { [] } unless options[:default]
32
- end
28
+ def derived?
29
+ type.nil?
33
30
  end
34
31
 
35
32
  def delegate
@@ -40,6 +37,10 @@ module Lutaml
40
37
  @options[:transform] || {}
41
38
  end
42
39
 
40
+ def method_name
41
+ @options[:method_name]
42
+ end
43
+
43
44
  def cast_type!(type)
44
45
  case type
45
46
  when Symbol
@@ -231,20 +232,11 @@ module Lutaml
231
232
 
232
233
  def serialize(value, format, options = {})
233
234
  return if value.nil?
235
+ return value if derived?
236
+ return serialize_array(value, format, options) if value.is_a?(Array)
237
+ return serialize_model(value, format, options) if type <= Serialize
234
238
 
235
- if value.is_a?(Array)
236
- value.map do |v|
237
- serialize(v, format, options)
238
- end
239
- elsif type <= Serialize
240
- if Utils.present?(value)
241
- type.public_send(:"as_#{format}", value, options)
242
- end
243
- else
244
- # Convert to Value instance if not already
245
- value = type.new(value) unless value.is_a?(Type::Value)
246
- value.send(:"to_#{format}")
247
- end
239
+ serialize_value(value, format)
248
240
  end
249
241
 
250
242
  def cast(value, format, options = {})
@@ -269,6 +261,41 @@ module Lutaml
269
261
  (format == :xml && value.is_a?(Lutaml::Model::XmlAdapter::XmlElement))
270
262
  end
271
263
 
264
+ def serialize_array(value, format, options)
265
+ value.map { |v| serialize(v, format, options) }
266
+ end
267
+
268
+ def serialize_model(value, format, options)
269
+ type.as(format, value, options) if Utils.present?(value)
270
+ end
271
+
272
+ def serialize_value(value, format)
273
+ value = type.new(value) unless value.is_a?(Type::Value)
274
+ value.send(:"to_#{format}")
275
+ end
276
+
277
+ def validate_presence!(type, method_name)
278
+ return if type || method_name
279
+
280
+ raise ArgumentError, "method or type must be set for an attribute"
281
+ end
282
+
283
+ def process_type!(type)
284
+ validate_type!(type)
285
+ @type = cast_type!(type)
286
+ end
287
+
288
+ def process_options!
289
+ validate_options!(@options)
290
+ @raw = !!@options[:raw]
291
+ set_default_for_collection if collection?
292
+ end
293
+
294
+ def set_default_for_collection
295
+ validate_collection_range
296
+ @options[:default] ||= -> { [] }
297
+ end
298
+
272
299
  def validate_options!(options)
273
300
  if (invalid_opts = options.keys - ALLOWED_OPTIONS).any?
274
301
  raise StandardError,
@@ -277,7 +304,8 @@ module Lutaml
277
304
 
278
305
  if options.key?(:pattern) && type != Lutaml::Model::Type::String
279
306
  raise StandardError,
280
- "Invalid option `pattern` given for `#{name}`, `pattern` is only allowed for :string type"
307
+ "Invalid option `pattern` given for `#{name}`, " \
308
+ "`pattern` is only allowed for :string type"
281
309
  end
282
310
 
283
311
  true
@@ -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
@@ -96,6 +96,11 @@ module Lutaml
96
96
 
97
97
  # Define an attribute for the model
98
98
  def attribute(name, type, options = {})
99
+ if type.is_a?(Hash)
100
+ options[:method_name] = type[:method]
101
+ type = nil
102
+ end
103
+
99
104
  attr = Attribute.new(name, type, options)
100
105
  attributes[name] = attr
101
106
 
@@ -106,6 +111,10 @@ module Lutaml
106
111
  options[:values],
107
112
  collection: options[:collection],
108
113
  )
114
+ elsif attr.derived? && name != attr.method_name
115
+ define_method(name) do
116
+ public_send(attr.method_name)
117
+ end
109
118
  else
110
119
  define_method(name) do
111
120
  instance_variable_get(:"@#{name}")
@@ -117,8 +126,6 @@ module Lutaml
117
126
  end
118
127
  end
119
128
 
120
- register_drop_method(name)
121
-
122
129
  attr
123
130
  end
124
131
 
@@ -285,6 +292,10 @@ module Lutaml
285
292
  end
286
293
  end
287
294
 
295
+ def as(format, instance, options = {})
296
+ public_send(:"as_#{format}", instance, options)
297
+ end
298
+
288
299
  def key_value(&block)
289
300
  Lutaml::Model::Config::KEY_VALUE_FORMATS.each do |format|
290
301
  mappings[format] ||= KeyValueMapping.new
@@ -486,8 +497,7 @@ module Lutaml
486
497
  return instance unless doc
487
498
 
488
499
  if options[:default_namespace].nil?
489
- options[:default_namespace] =
490
- mappings_for(:xml)&.namespace_uri
500
+ options[:default_namespace] = mappings_for(:xml)&.namespace_uri
491
501
  end
492
502
  mappings = options[:mappings] || mappings_for(:xml).mappings
493
503
 
@@ -519,6 +529,7 @@ module Lutaml
519
529
  raise "Attribute '#{rule.to}' not found in #{self}" unless valid_rule?(rule)
520
530
 
521
531
  attr = attribute_for_rule(rule)
532
+ next if attr&.derived?
522
533
 
523
534
  value = if rule.raw_mapping?
524
535
  doc.root.inner_xml
@@ -705,6 +716,8 @@ module Lutaml
705
716
  end
706
717
 
707
718
  self.class.attributes.each do |name, attr|
719
+ next if attr.derived?
720
+
708
721
  value = if attrs.key?(name) || attrs.key?(name.to_s)
709
722
  attr_value(attrs, name, attr)
710
723
  else
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Lutaml
4
4
  module Model
5
- VERSION = "0.6.3"
5
+ VERSION = "0.6.5"
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 %}
@@ -7,6 +7,10 @@ RSpec.describe Lutaml::Model::Attribute do
7
7
  described_class.new("name", :string)
8
8
  end
9
9
 
10
+ let(:method_attr) do
11
+ described_class.new("name", nil, method_name: nil)
12
+ end
13
+
10
14
  let(:test_record_class) do
11
15
  Class.new(Lutaml::Model::Serializable) do
12
16
  attribute :age, :integer
@@ -33,6 +37,13 @@ RSpec.describe Lutaml::Model::Attribute do
33
37
  .to("avatar.png")
34
38
  end
35
39
 
40
+ it "raises error if both type and method_name are not given" do
41
+ expect { method_attr }.to raise_error(
42
+ ArgumentError,
43
+ "method or type must be set for an attribute",
44
+ )
45
+ end
46
+
36
47
  describe "#validate_options!" do
37
48
  let(:validate_options) { name_attr.method(:validate_options!) }
38
49
 
@@ -103,7 +114,25 @@ RSpec.describe Lutaml::Model::Attribute do
103
114
  end
104
115
  end
105
116
 
106
- describe "#default?" do
117
+ describe "#derived?" do
118
+ context "when type is set" do
119
+ let(:attribute) { described_class.new("name", :string) }
120
+
121
+ it "returns false" do
122
+ expect(attribute.derived?).to be(false)
123
+ end
124
+ end
125
+
126
+ context "when type is nil and method_name is set" do
127
+ let(:attribute) { described_class.new("name", nil, method_name: :tmp) }
128
+
129
+ it "returns true" do
130
+ expect(attribute.derived?).to be(true)
131
+ end
132
+ end
133
+ end
134
+
135
+ describe "#default" do
107
136
  context "when default is not set" do
108
137
  let(:attribute) { described_class.new("name", :string) }
109
138
 
@@ -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
@@ -112,13 +112,42 @@ RSpec.describe Lutaml::Model::Serializable do
112
112
  end
113
113
 
114
114
  describe ".attribute" do
115
- subject(:mapper) { described_class.new }
115
+ before do
116
+ stub_const("TestClass", Class.new(described_class))
117
+ end
118
+
119
+ context "when method_name is given" do
120
+ let(:attribute) do
121
+ TestClass.attribute("test", method: :foobar)
122
+ end
116
123
 
117
- it "adds the attribute and getter setter for that attribute" do
118
- expect { described_class.attribute("foo", Lutaml::Model::Type::String) }
119
- .to change { described_class.attributes.keys }.from([]).to(["foo"])
120
- .and change { mapper.respond_to?(:foo) }.from(false).to(true)
121
- .and change { mapper.respond_to?(:foo=) }.from(false).to(true)
124
+ it "adds derived attribute" do
125
+ expect { attribute }
126
+ .to change { TestClass.attributes["test"] }
127
+ .from(nil)
128
+ .to(Lutaml::Model::Attribute)
129
+ end
130
+
131
+ it "returns true for derived?" do
132
+ expect(attribute.derived?).to be(true)
133
+ end
134
+ end
135
+
136
+ context "when type is given" do
137
+ let(:attribute) do
138
+ TestClass.attribute("foo", Lutaml::Model::Type::String)
139
+ end
140
+
141
+ it "adds the attribute and getter setter for that attribute" do
142
+ expect { attribute }
143
+ .to change { TestClass.attributes.keys }.from([]).to(["foo"])
144
+ .and change { TestClass.new.respond_to?(:foo) }.from(false).to(true)
145
+ .and change { TestClass.new.respond_to?(:foo=) }.from(false).to(true)
146
+ end
147
+
148
+ it "returns false for derived?" do
149
+ expect(attribute.derived?).to be(false)
150
+ end
122
151
  end
123
152
  end
124
153
 
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ module DerivedAttributesSpecs
6
+ class Ceramic < Lutaml::Model::Serializable
7
+ attribute :name, :string
8
+ attribute :value, :float
9
+ end
10
+
11
+ class CeramicCollection < Lutaml::Model::Serializable
12
+ attribute :items, Ceramic, collection: true
13
+ attribute :total_value, method: :total_value
14
+
15
+ # Derived property
16
+ def total_value
17
+ items.sum(&:value)
18
+ end
19
+
20
+ xml do
21
+ root "ceramic-collection"
22
+ map_element "total-value", to: :total_value
23
+ map_element "item", to: :items
24
+ end
25
+ end
26
+ end
27
+
28
+ RSpec.describe "XML::DerivedAttributes" do
29
+ let(:xml) do
30
+ <<~XML.strip
31
+ <ceramic-collection>
32
+ <total-value>2500.0</total-value>
33
+ <item>
34
+ <name>Ancient Vase</name>
35
+ <value>1500.0</value>
36
+ </item>
37
+ <item>
38
+ <name>Historic Bowl</name>
39
+ <value>1000.0</value>
40
+ </item>
41
+ </ceramic-collection>
42
+ XML
43
+ end
44
+
45
+ let(:ancient_vase) do
46
+ DerivedAttributesSpecs::Ceramic.new(name: "Ancient Vase", value: 1500.0)
47
+ end
48
+
49
+ let(:historic_bowl) do
50
+ DerivedAttributesSpecs::Ceramic.new(name: "Historic Bowl", value: 1000.0)
51
+ end
52
+
53
+ let(:ceramic_collection) do
54
+ DerivedAttributesSpecs::CeramicCollection.new(
55
+ items: [ancient_vase, historic_bowl],
56
+ )
57
+ end
58
+
59
+ describe ".from_xml" do
60
+ let(:parsed) do
61
+ DerivedAttributesSpecs::CeramicCollection.from_xml(xml)
62
+ end
63
+
64
+ it "correctly parses items" do
65
+ expect(parsed).to eq(ceramic_collection)
66
+ end
67
+
68
+ it "correctly calculates total-value" do
69
+ expect(parsed.total_value).to eq(2500)
70
+ end
71
+ end
72
+
73
+ describe ".to_xml" do
74
+ it "convert to correct xml" do
75
+ expect(ceramic_collection.to_xml).to eq(xml)
76
+ end
77
+ end
78
+ 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.3
4
+ version: 0.6.5
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-14 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
@@ -253,6 +257,7 @@ files:
253
257
  - spec/lutaml/model/utils_spec.rb
254
258
  - spec/lutaml/model/validation_spec.rb
255
259
  - spec/lutaml/model/with_child_mapping_spec.rb
260
+ - spec/lutaml/model/xml/derived_attributes_spec.rb
256
261
  - spec/lutaml/model/xml_adapter/nokogiri_adapter_spec.rb
257
262
  - spec/lutaml/model/xml_adapter/oga_adapter_spec.rb
258
263
  - spec/lutaml/model/xml_adapter/ox_adapter_spec.rb