lutaml-model 0.6.4 → 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: 16b8569b5f2b450dfa8218c1c55a2175f154790220a54536bc9acbf6bc718312
4
- data.tar.gz: d9d62e793690a72856e2663a67eb4eeb9cd8b909a861a91b093d8b849674a983
3
+ metadata.gz: 07d4f2ac62702a8ac27070d4d3246e59c367937cf50b649979ecb3c525f9b60e
4
+ data.tar.gz: 31adf8c870b4196ea69a065559ebbb5c2f35719aba3256597d9e24474bcbfa57
5
5
  SHA512:
6
- metadata.gz: 5e491d76d913445b9929b4e1939becc31c4b1ccb656aeaaa3e83caa20bf3c5e53142c3cc45f1a82b0a85d4b7111ab6994d5a5c03588c4f42dd8f76d101c95db0
7
- data.tar.gz: e18a44f0047929b93c2bc66c82f8942d3cf48818c6597722bba0b78107c2630aa59f84b3b4034a105155d889e64fa7e8f11881fbc9eca076b3103c21f8f189cf
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-15 02:54:02 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:
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
@@ -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
@@ -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}")
@@ -283,6 +292,10 @@ module Lutaml
283
292
  end
284
293
  end
285
294
 
295
+ def as(format, instance, options = {})
296
+ public_send(:"as_#{format}", instance, options)
297
+ end
298
+
286
299
  def key_value(&block)
287
300
  Lutaml::Model::Config::KEY_VALUE_FORMATS.each do |format|
288
301
  mappings[format] ||= KeyValueMapping.new
@@ -484,8 +497,7 @@ module Lutaml
484
497
  return instance unless doc
485
498
 
486
499
  if options[:default_namespace].nil?
487
- options[:default_namespace] =
488
- mappings_for(:xml)&.namespace_uri
500
+ options[:default_namespace] = mappings_for(:xml)&.namespace_uri
489
501
  end
490
502
  mappings = options[:mappings] || mappings_for(:xml).mappings
491
503
 
@@ -517,6 +529,7 @@ module Lutaml
517
529
  raise "Attribute '#{rule.to}' not found in #{self}" unless valid_rule?(rule)
518
530
 
519
531
  attr = attribute_for_rule(rule)
532
+ next if attr&.derived?
520
533
 
521
534
  value = if rule.raw_mapping?
522
535
  doc.root.inner_xml
@@ -703,6 +716,8 @@ module Lutaml
703
716
  end
704
717
 
705
718
  self.class.attributes.each do |name, attr|
719
+ next if attr.derived?
720
+
706
721
  value = if attrs.key?(name) || attrs.key?(name.to_s)
707
722
  attr_value(attrs, name, attr)
708
723
  else
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Lutaml
4
4
  module Model
5
- VERSION = "0.6.4"
5
+ VERSION = "0.6.5"
6
6
  end
7
7
  end
@@ -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
 
@@ -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
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lutaml-model
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.4
4
+ version: 0.6.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
@@ -257,6 +257,7 @@ files:
257
257
  - spec/lutaml/model/utils_spec.rb
258
258
  - spec/lutaml/model/validation_spec.rb
259
259
  - spec/lutaml/model/with_child_mapping_spec.rb
260
+ - spec/lutaml/model/xml/derived_attributes_spec.rb
260
261
  - spec/lutaml/model/xml_adapter/nokogiri_adapter_spec.rb
261
262
  - spec/lutaml/model/xml_adapter/oga_adapter_spec.rb
262
263
  - spec/lutaml/model/xml_adapter/ox_adapter_spec.rb