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 +4 -4
- data/.rubocop_todo.yml +8 -9
- data/Gemfile +1 -0
- data/README.adoc +281 -40
- data/lib/lutaml/model/attribute.rb +52 -24
- data/lib/lutaml/model/error/liquid_not_enabled_error.rb +9 -0
- data/lib/lutaml/model/error.rb +1 -0
- data/lib/lutaml/model/liquefiable.rb +15 -2
- data/lib/lutaml/model/serialize.rb +17 -4
- data/lib/lutaml/model/version.rb +1 -1
- data/spec/fixtures/liquid_templates/_ceramic.liquid +6 -0
- data/spec/fixtures/liquid_templates/_ceramics.liquid +3 -0
- data/spec/fixtures/liquid_templates/_ceramics_in_one.liquid +4 -0
- data/spec/lutaml/model/attribute_spec.rb +30 -1
- data/spec/lutaml/model/liquefiable_spec.rb +152 -14
- data/spec/lutaml/model/serializable_spec.rb +35 -6
- data/spec/lutaml/model/xml/derived_attributes_spec.rb +78 -0
- data/spec/spec_helper.rb +1 -0
- metadata +7 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 07d4f2ac62702a8ac27070d4d3246e59c367937cf50b649979ecb3c525f9b60e
|
4
|
+
data.tar.gz: 31adf8c870b4196ea69a065559ebbb5c2f35719aba3256597d9e24474bcbfa57
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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-
|
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:
|
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:
|
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:
|
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:
|
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:
|
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:
|
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:
|
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
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
|
-
|
454
|
+
WARNING: Decimal is an optional feature.
|
417
455
|
|
418
|
-
|
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
|
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
|
-
`
|
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
|
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 :
|
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
|
-
|
4612
|
-
|
4613
|
-
|
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
|
-
|
4616
|
-
|
4617
|
-
puts
|
4618
|
-
#
|
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
|
-
|
23
|
+
validate_presence!(type, options[:method_name])
|
24
|
+
process_type!(type) if type
|
25
|
+
process_options!
|
26
|
+
end
|
28
27
|
|
29
|
-
|
30
|
-
|
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
|
-
|
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}`,
|
307
|
+
"Invalid option `pattern` given for `#{name}`, " \
|
308
|
+
"`pattern` is only allowed for :string type"
|
281
309
|
end
|
282
310
|
|
283
311
|
true
|
data/lib/lutaml/model/error.rb
CHANGED
@@ -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
|
data/lib/lutaml/model/version.rb
CHANGED
@@ -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 "#
|
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
|
30
|
-
dummy.class.
|
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
|
73
|
-
dummy.
|
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
|
-
|
82
|
-
|
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
|
-
|
87
|
-
|
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
|
-
|
91
|
-
|
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
|
-
|
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
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
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
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.
|
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-
|
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
|