lutaml-model 0.6.4 → 0.6.6

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 16b8569b5f2b450dfa8218c1c55a2175f154790220a54536bc9acbf6bc718312
4
- data.tar.gz: d9d62e793690a72856e2663a67eb4eeb9cd8b909a861a91b093d8b849674a983
3
+ metadata.gz: 5097f4bf4d2b95fe089f40da159330218a69558acc9e3401784f12fc566bae00
4
+ data.tar.gz: 45a29395158a5362446677cfa7340b2e739a715ef7639c934f9a93a3e2b166cb
5
5
  SHA512:
6
- metadata.gz: 5e491d76d913445b9929b4e1939becc31c4b1ccb656aeaaa3e83caa20bf3c5e53142c3cc45f1a82b0a85d4b7111ab6994d5a5c03588c4f42dd8f76d101c95db0
7
- data.tar.gz: e18a44f0047929b93c2bc66c82f8942d3cf48818c6597722bba0b78107c2630aa59f84b3b4034a105155d889e64fa7e8f11881fbc9eca076b3103c21f8f189cf
6
+ metadata.gz: 7bccace8ffeabe88a60b06e610e0b168772981d4a2b6db956f9b70f295f5522c909e0f0001fed6396fdbd44169d9a191b6b5de2d94cf6a2135bbd6a226dd7669
7
+ data.tar.gz: 19ab03f407b11348eaf01f37db05151e82de56c0c9bcac1fb2e76f27597b7eea450e6d600216be5260ac04ccbe3111a858409a7a9ed33dc559397a6b885ad8fe
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 13:26:38 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: 468
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
@@ -270,19 +270,17 @@ Or install it yourself as:
270
270
  gem install lutaml-model
271
271
  ----
272
272
 
273
- == Data model class
273
+ == Model
274
274
 
275
- === Definition
276
-
277
- ==== General
275
+ === General
278
276
 
279
- There are two ways to define a data model in Lutaml::Model:
277
+ There are two ways to define an information model in Lutaml::Model:
280
278
 
281
279
  * Inheriting from the `Lutaml::Model::Serializable` class
282
280
  * Including the `Lutaml::Model::Serialize` module
283
281
 
284
282
  [[define-through-inheritance]]
285
- ==== Definition through inheritance
283
+ === Definition through inheritance
286
284
 
287
285
  The simplest way to define a model is to create a class that inherits from
288
286
  `Lutaml::Model::Serializable`.
@@ -301,7 +299,7 @@ end
301
299
  ----
302
300
 
303
301
  [[define-through-inclusion]]
304
- ==== Definition through inclusion
302
+ === Definition through inclusion
305
303
 
306
304
  If the model class already has a super class that it inherits from, the model
307
305
  can be extended using the `Lutaml::Model::Serialize` module.
@@ -344,25 +342,14 @@ same class and all their attributes are equal.
344
342
  ----
345
343
 
346
344
 
345
+ == Value types
347
346
 
348
- == Defining attributes
349
-
350
- === Supported attribute value types
347
+ === General types
351
348
 
352
- ==== General types
353
-
354
- Lutaml::Model supports the following attribute types, they can be
355
- referred by a string, a symbol, or their class constant.
349
+ Lutaml::Model supports the following attribute value types.
356
350
 
357
351
  Every type has a corresponding Ruby class and a serialization format type.
358
352
 
359
- Syntax:
360
-
361
- [source,ruby]
362
- ----
363
- attribute :name_of_attribute, {symbol | string | class}
364
- ----
365
-
366
353
  .Mapping between Lutaml::Model::Type classes, Ruby equivalents and serialization format types
367
354
  |===
368
355
  | Lutaml::Model::Type | Ruby class | XML | JSON | YAML | Example value
@@ -385,33 +372,7 @@ attribute :name_of_attribute, {symbol | string | class}
385
372
  |===
386
373
 
387
374
 
388
- .Defining attributes with supported types via symbol, string and class
389
- [example]
390
- ====
391
- [source,ruby]
392
- ----
393
- class Studio < Lutaml::Model::Serializable
394
- # The following are equivalent
395
- attribute :location, :string
396
- attribute :potter, "String"
397
- attribute :kiln, :string
398
- end
399
- ----
400
-
401
- [source,ruby]
402
- ----
403
- > s = Studio.new(location: 'London', potter: 'John Doe', kiln: 'Kiln 1')
404
- > # <Studio:0x0000000104ac7240 @location="London", @potter="John Doe", @kiln="Kiln 1">
405
- > s.location
406
- > # "London"
407
- > s.potter
408
- > # "John Doe"
409
- > s.kiln
410
- > # "Kiln 1"
411
- ----
412
- ====
413
-
414
- ==== Decimal type
375
+ === Decimal type
415
376
 
416
377
  WARNING: Decimal is an optional feature.
417
378
 
@@ -434,7 +395,7 @@ If the `bigdecimal` library is not loaded, usage of the `Decimal` type will
434
395
  raise a `Lutaml::Model::TypeNotSupportedError`.
435
396
 
436
397
 
437
- ==== Custom type
398
+ === Custom type
438
399
 
439
400
  A custom class can be used as an attribute type. The custom class must inherit
440
401
  from `Lutaml::Model::Type::Value` or a class that inherits from it.
@@ -453,7 +414,6 @@ set as `value`. Casts the value to the custom type.
453
414
  string). Takes the internal `value` and converts it into an output suitable for
454
415
  serialization.
455
416
 
456
-
457
417
  .Using a custom value type to normalize a postcode with minimal methods
458
418
  [example]
459
419
  ====
@@ -482,8 +442,7 @@ end
482
442
  ----
483
443
  ====
484
444
 
485
-
486
- ==== Serialization of custom types
445
+ === Serialization of custom types
487
446
 
488
447
  The serialization of custom types can be made to differ per serialization format
489
448
  by defining methods in the class definitions. This requires additional methods
@@ -570,7 +529,105 @@ part lost due to the inability of JSON to handle high-precision date-time.
570
529
  ====
571
530
 
572
531
 
573
- === Attribute as a collection
532
+ == Attributes
533
+
534
+
535
+ === Basic attributes
536
+
537
+ An attribute is the basic building block of a model. It is a named value that
538
+ stores a single piece of data (which may be one or multiple pieces of data).
539
+
540
+ An attribute only accepts the type of value defined in the attribute definition.
541
+
542
+ The attribute value type can be one of the following:
543
+
544
+ * Value (inherits from Lutaml::Model::Value)
545
+ * Model (inherits from Lutaml::Model::Serializable)
546
+
547
+ Syntax:
548
+
549
+ [source,ruby]
550
+ ----
551
+ attribute :name_of_attribute, Type
552
+ ----
553
+
554
+ Where,
555
+
556
+ `name_of_attribute`:: The defined name of the attribute.
557
+ `Type`:: The type of the attribute.
558
+
559
+ .Using the `attribute` class method to define simple attributes
560
+ [example]
561
+ ====
562
+ [source,ruby]
563
+ ----
564
+ class Studio < Lutaml::Model::Serializable
565
+ attribute :name, :string
566
+ attribute :address, :string
567
+ attribute :established, :date
568
+ end
569
+ ----
570
+
571
+ [source,ruby]
572
+ ----
573
+ s = Studio.new(name: 'Pottery Studio', address: '123 Clay St', established: Date.new(2020, 1, 1))
574
+ puts s.name
575
+ #=> "Pottery Studio"
576
+ puts s.address
577
+ #=> "123 Clay St"
578
+ puts s.established
579
+ #=> <Date: 2020-01-01>
580
+ ----
581
+ ====
582
+
583
+
584
+ An attribute with a defined value type also accepts values that are of a class that
585
+ is a subclass of the defined type.
586
+
587
+ This means that the assigned `Type` accepts polymorphic classes as long as the
588
+ assigned instance is of a class that either inherits from the declared type or
589
+ matches it.
590
+
591
+ .Using a superclass type receive child object
592
+ [example]
593
+ ====
594
+ [source,ruby]
595
+ ----
596
+ class Studio < Lutaml::Model::Serializable
597
+ attribute :name, :string
598
+ end
599
+
600
+ class CeramicStudio < Studio
601
+ attribute :clay_type, :string
602
+ end
603
+
604
+ class PotteryClass < Lutaml::Model::Serializable
605
+ attribute :studio, Studio
606
+ end
607
+ ----
608
+
609
+ [source,ruby]
610
+ ----
611
+ # This works
612
+ > s = Studio.new(name: 'Pottery Studio')
613
+ > p = PotteryClass.new(studio: s)
614
+ > p.studio
615
+ # => <Studio:0x0000000104ac7240 @name="Pottery Studio", @address=nil, @established=nil>
616
+
617
+ # A subclass of Studio is also valid
618
+ > s = CeramicStudio.new(name: 'Ceramic World', clay_type: 'Red')
619
+ > p = PotteryClass.new(studio: s)
620
+ > p.studio
621
+ # => <CeramicStudio:0x0000000104ac7240 @name="Ceramic World", @address=nil, @established=nil, @clay_type="Red">
622
+ > p.studio.name
623
+ # => "Ceramic World"
624
+ > p.studio.clay_type
625
+ # => "Red"
626
+ ----
627
+ ====
628
+
629
+
630
+ === Collection attributes
574
631
 
575
632
  Define attributes as collections (arrays or hashes) to store multiple values
576
633
  using the `collection` option.
@@ -644,61 +701,122 @@ end
644
701
  ----
645
702
  ====
646
703
 
704
+ === Derived attributes
647
705
 
648
- === Sequence within XmlMapping
706
+ A derived attribute is computed dynamically based on an instance method instead of storing a static value. It is defined using the `method:` option.
649
707
 
650
- The sequence option enforces that the defined components must appear in a specified order.
708
+ Syntax:
651
709
 
652
- NOTE: `sequence` only works within XML and only supports `map_element` mappings.
710
+ [source,ruby]
711
+ ----
712
+ attribute :name_of_attribute, method: :instance_method_name
713
+ ----
653
714
 
654
- .Using the `sequence` keyword to define a set of elements in desired order.
715
+ .Defining methods as attributes
655
716
  [example]
656
717
  ====
657
718
  [source,ruby]
658
719
  ----
659
- class Kiln < Lutaml::Model::Serializable
660
- attribute :id, :string
661
- attribute :name, :string
662
- attribute :type, :string
663
- attribute :color, :string
720
+ class Invoice < Lutaml::Model::Serializable
721
+ attribute :subtotal, :float
722
+ attribute :tax, :float
723
+ attribute :total, method: :total_value
664
724
 
665
- xml do
666
- sequence do
667
- map_element :id, to: :id
668
- map_element :name, to: :name
669
- map_element :type, to: :type
670
- map_element :color, to: :color
671
- end
725
+ def total_value
726
+ subtotal + tax
672
727
  end
673
728
  end
674
729
 
675
- class KilnCollection < Lutaml::Model::Serializable
676
- attribute :kiln, Kiln, collection: 1..2
730
+ i = Invoice.new(subtotal: 100.0, tax: 12.0)
731
+ i.total
732
+ #=> 112.0
677
733
 
678
- xml do
679
- root "collection"
680
- map_element "kiln", to: :kiln
681
- end
734
+ puts i.to_yaml
735
+ #=> ---
736
+ #=> subtotal: 100.0
737
+ #=> tax: 12.0
738
+ #=> total: 112.0
739
+ ----
740
+ ====
741
+
742
+
743
+ === Choice attributes
744
+
745
+ The `choice` directive allows specifying that elements from the specified range are included.
746
+
747
+ NOTE: Attribute-level definitions are supported. This can be used with both
748
+ `key_value` and `xml` mappings.
749
+
750
+ Syntax:
751
+
752
+ [source,ruby]
753
+ ----
754
+ choice(min: {min}, max: {max}) do
755
+ {block}
682
756
  end
683
757
  ----
684
758
 
759
+ Where,
760
+
761
+ `min`:: The minimum number of elements that must be included.
762
+ `max`:: The maximum number of elements that can be included.
763
+ `block`:: The block of elements that must be included. The block can contain
764
+ multiple `attribute` and `choice` directives.
765
+
766
+ .Using the `choice` directive to define a set of attributes with a range
767
+ [example]
768
+ ====
685
769
  [source,ruby]
686
770
  ----
687
- > parsed = Kiln.from_xml("<Kiln> <id>1</id> <name>Nick</name> <type>Hard</type> <color>Black</color> </Kiln>")
688
- > # parsed.not_to raise_error
771
+ class Studio < Lutaml::Model::Serializable
772
+ choice(min: 1, max: 3) do
773
+ choice(min: 1, max: 2) do
774
+ attribute :prefix, :string
775
+ attribute :forename, :string
776
+ end
777
+
778
+ attribute :completeName, :string
779
+ end
780
+ end
689
781
  ----
782
+
783
+ This means that the `Studio` class must have at least one and at most three
784
+ attributes.
785
+
786
+ * The first choice must have at least one and at most two attributes.
787
+ * The second attribute is the `completeName`.
788
+ * The first choice can have either the `prefix` and `forename` attributes or just the `forename` attribute.
789
+ * The last attribute `completeName` is optional.
690
790
  ====
691
791
 
692
792
 
693
- === Reusable Classes with Import
694
793
 
695
- Lutaml lets you create reusable element and attribute collections using `no_root`. These can be imported into other models using:
794
+ === Importable models for reuse
795
+
796
+ An importable model is a model that can be imported into another model using the
797
+ `import_*` directive.
798
+
799
+ Such a model is specified by setting the model's XML serialization configuration
800
+ with the `no_root` directive.
801
+
802
+ As a result, the model can be imported into another model using the following
803
+ directives:
804
+
805
+ `import_model`:: imports both attributes and mappings.
696
806
 
697
- - `import_model`: imports both attributes and mappings
698
- - `import_model_attributes`: imports only attributes
699
- - `import_model_mappings`: imports only mappings
807
+ `import_model_attributes`:: imports only attributes.
808
+
809
+ `import_model_mappings`:: imports only mappings.
810
+
811
+ NOTE: This feature only works with XML for now. The import order determines how
812
+ elements and attributes are overwritten.
813
+
814
+ Models with `no_root` can only be parsed through **parent models**.
815
+ Direct calling `NoRootModel.from_xml` will raise a `NoRootMappingError`.
816
+
817
+ Namespaces are not supported in importable models. If `namespace` is defined with
818
+ `no_root`, `NoRootNamespaceError` will raise.
700
819
 
701
- NOTE: This feature works with XML. Import order determines how elements and attributes are overwritten.
702
820
 
703
821
  [example]
704
822
  ====
@@ -756,39 +874,11 @@ end
756
874
  > parsed = GroupOfItems.from_xml(xml)
757
875
  > # Lutaml::Model::NoRootMappingError: "GroupOfItems has `no_root`, it allowed only for reusable models"
758
876
  ----
759
-
760
- NOTE: Models with `no_root` can only be parsed through **Parent Models**. Direct calling `from_xml` will raise `NoRootMappingError`.
761
- And if `namespace` is defined with `no_root`, `NoRootNamespaceError` will raise.
762
-
763
- ====
764
-
765
-
766
- === Choice
767
-
768
- The `choice` option ensures that elements from the specified range are included.
769
-
770
- NOTE: Attribute-level definitions are supported. This can be used with both key_value and xml mappings.
771
-
772
- .Using the `choice` option to define a set of attributes with a range.
773
- [example]
774
877
  ====
775
- [source,ruby]
776
- ----
777
- class Studio < Lutaml::Model::Serializable
778
- choice(min: 1, max: 3) do
779
- choice(min: 1, max: 2) do
780
- attribute :prefix, :string
781
- attribute :forename, :string
782
- end
783
878
 
784
- attribute :completeName, :string
785
- end
786
- end
787
- ----
788
- ====
789
879
 
790
880
 
791
- === Attribute value validation
881
+ === Value validation
792
882
 
793
883
  ==== General
794
884
 
@@ -1100,6 +1190,56 @@ end
1100
1190
  ----
1101
1191
  ====
1102
1192
 
1193
+
1194
+ ==== Ommiting root element
1195
+
1196
+ The root element can be omitted by using the `no_root` method.
1197
+
1198
+ When `no_root` is used, only `map_element` can be used because without a root
1199
+ element there cannot be attributes.
1200
+
1201
+ Syntax:
1202
+
1203
+ [source,ruby]
1204
+ ----
1205
+ xml do
1206
+ no_root
1207
+ end
1208
+ ----
1209
+
1210
+ [example]
1211
+ ====
1212
+ [source,ruby]
1213
+ ----
1214
+ class NameAndCode < Lutaml::Model::Serializable
1215
+ attribute :name, :string
1216
+ attribute :code, :string
1217
+
1218
+ xml do
1219
+ no_root
1220
+ map_element "code", to: :code
1221
+ map_element "name", to: :name
1222
+ end
1223
+ end
1224
+ ----
1225
+
1226
+ [source,xml]
1227
+ ----
1228
+ <name>Name</name>
1229
+ <code>ID-001</code>
1230
+ ----
1231
+
1232
+ [source,ruby]
1233
+ ----
1234
+ > parsed = NameAndCode.from_xml(xml)
1235
+ > # <NameAndCode:0x0000000107a3ca70 @code="ID-001", @name="Name">
1236
+ > parsed.to_xml
1237
+ > # <code>ID-001</code><name>Name</name>
1238
+ ----
1239
+ ====
1240
+
1241
+
1242
+
1103
1243
  [[xml-map-all]]
1104
1244
  ==== Mapping all XML content
1105
1245
 
@@ -1882,6 +2022,104 @@ end
1882
2022
  ====
1883
2023
 
1884
2024
 
2025
+
2026
+ ==== Sequence
2027
+
2028
+ The `sequence` directive specifies that the defined attributes must appear in a
2029
+ specified order in XML.
2030
+
2031
+ NOTE: Sequence only supports `map_element` mappings.
2032
+
2033
+ Syntax:
2034
+
2035
+ [source,ruby]
2036
+ ----
2037
+ xml do
2038
+ sequence do
2039
+ map_element 'xml_element_name_1', to: :name_of_attribute_1
2040
+ map_element 'xml_element_name_2', to: :name_of_attribute_2
2041
+ # Add more map_element lines as needed to establish a complete sequence
2042
+ end
2043
+ end
2044
+ ----
2045
+
2046
+ The appearance of the elements in the XML document must match the order defined
2047
+ in the `sequence` block. In this case, the `<xml_element_name_1>` element
2048
+ should appear before the `<xml_element_name_2>` element.
2049
+
2050
+ .Using the `sequence` keyword to define a set of elements in desired order.
2051
+ [example]
2052
+ ====
2053
+ [source,ruby]
2054
+ ----
2055
+ class Kiln < Lutaml::Model::Serializable
2056
+ attribute :id, :string
2057
+ attribute :name, :string
2058
+ attribute :type, :string
2059
+ attribute :color, :string
2060
+
2061
+ xml do
2062
+ sequence do
2063
+ map_element :id, to: :id
2064
+ map_element :name, to: :name
2065
+ map_element :type, to: :type
2066
+ map_element :color, to: :color
2067
+ end
2068
+ end
2069
+ end
2070
+
2071
+ class KilnCollection < Lutaml::Model::Serializable
2072
+ attribute :kiln, Kiln, collection: 1..2
2073
+
2074
+ xml do
2075
+ root "collection"
2076
+ map_element "kiln", to: :kiln
2077
+ end
2078
+ end
2079
+ ----
2080
+
2081
+ [source,xml]
2082
+ ----
2083
+ <collection>
2084
+ <kiln>
2085
+ <id>1</id>
2086
+ <name>Nick</name>
2087
+ <type>Hard</type>
2088
+ <color>Black</color>
2089
+ </kiln>
2090
+ <kiln>
2091
+ <id>2</id>
2092
+ <name>John</name>
2093
+ <type>Soft</type>
2094
+ <color>White</color>
2095
+ </kiln>
2096
+ </collection>
2097
+ ----
2098
+
2099
+ [source,ruby]
2100
+ ----
2101
+ > parsed = Kiln.from_xml(xml)
2102
+ # => [
2103
+ #<Kiln:0x0000000104ac7240 @id="1", @name="Nick", @type="Hard", @color="Black">,
2104
+ #<Kiln:0x0000000104ac7240 @id="2", @name="John", @type="Soft", @color="White">
2105
+ #]
2106
+
2107
+ > bad_xml = <<~HERE
2108
+ <collection>
2109
+ <kiln>
2110
+ <name>Nick</name>
2111
+ <id>1</id>
2112
+ <color>Black</color>
2113
+ <type>Hard</type>
2114
+ </kiln>
2115
+ </collection>
2116
+ HERE
2117
+ > parsed = Kiln.from_xml(bad_xml)
2118
+ # => Lutaml::Model::ValidationError: Element 'name' is out of order in 'kiln' element
2119
+ ----
2120
+ ====
2121
+
2122
+
1885
2123
  [[xml-schema-location]]
1886
2124
  ==== Automatic support of `xsi:schemaLocation`
1887
2125
 
@@ -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
@@ -159,7 +160,9 @@ module Lutaml
159
160
  # Use the default value if the value is nil
160
161
  value = default if value.nil?
161
162
 
162
- valid_value!(value) && valid_collection!(value, self) && valid_pattern!(value)
163
+ valid_value!(value) &&
164
+ valid_collection!(value, self) &&
165
+ valid_pattern!(value)
163
166
  end
164
167
 
165
168
  def validate_collection_range
@@ -181,12 +184,14 @@ module Lutaml
181
184
 
182
185
  if range.begin.negative?
183
186
  raise ArgumentError,
184
- "Invalid collection range: #{range}. Begin must be non-negative."
187
+ "Invalid collection range: #{range}. " \
188
+ "Begin must be non-negative."
185
189
  end
186
190
 
187
191
  if range.end && range.end < range.begin
188
192
  raise ArgumentError,
189
- "Invalid collection range: #{range}. End must be greater than or equal to begin."
193
+ "Invalid collection range: #{range}. " \
194
+ "End must be greater than or equal to begin."
190
195
  end
191
196
  end
192
197
 
@@ -231,29 +236,20 @@ module Lutaml
231
236
 
232
237
  def serialize(value, format, options = {})
233
238
  return if value.nil?
239
+ return value if derived?
240
+ return serialize_array(value, format, options) if value.is_a?(Array)
241
+ return serialize_model(value, format, options) if type <= Serialize
234
242
 
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
243
+ serialize_value(value, format)
248
244
  end
249
245
 
250
246
  def cast(value, format, options = {})
251
247
  return value if type <= Serialize && value.is_a?(type.model)
252
248
 
253
249
  value ||= [] if collection?
254
- if value.is_a?(Array)
255
- value.map { |v| cast(v, format, options) }
256
- elsif type <= Serialize && castable?(value, format)
250
+ return value.map { |v| cast(v, format, options) } if value.is_a?(Array)
251
+
252
+ if type <= Serialize && castable?(value, format)
257
253
  type.apply_mappings(value, format, options)
258
254
  elsif !value.nil? && !value.is_a?(type)
259
255
  type.send(:"from_#{format}", value)
@@ -269,6 +265,44 @@ module Lutaml
269
265
  (format == :xml && value.is_a?(Lutaml::Model::XmlAdapter::XmlElement))
270
266
  end
271
267
 
268
+ def serialize_array(value, format, options)
269
+ value.map { |v| serialize(v, format, options) }
270
+ end
271
+
272
+ def serialize_model(value, format, options)
273
+ return unless Utils.present?(value)
274
+ return value.class.as(format, value, options) if value.is_a?(type)
275
+
276
+ type.as(format, value, options)
277
+ end
278
+
279
+ def serialize_value(value, format)
280
+ value = type.new(value) unless value.is_a?(Type::Value)
281
+ value.send(:"to_#{format}")
282
+ end
283
+
284
+ def validate_presence!(type, method_name)
285
+ return if type || method_name
286
+
287
+ raise ArgumentError, "method or type must be set for an attribute"
288
+ end
289
+
290
+ def process_type!(type)
291
+ validate_type!(type)
292
+ @type = cast_type!(type)
293
+ end
294
+
295
+ def process_options!
296
+ validate_options!(@options)
297
+ @raw = !!@options[:raw]
298
+ set_default_for_collection if collection?
299
+ end
300
+
301
+ def set_default_for_collection
302
+ validate_collection_range
303
+ @options[:default] ||= -> { [] }
304
+ end
305
+
272
306
  def validate_options!(options)
273
307
  if (invalid_opts = options.keys - ALLOWED_OPTIONS).any?
274
308
  raise StandardError,
@@ -277,7 +311,8 @@ module Lutaml
277
311
 
278
312
  if options.key?(:pattern) && type != Lutaml::Model::Type::String
279
313
  raise StandardError,
280
- "Invalid option `pattern` given for `#{name}`, `pattern` is only allowed for :string type"
314
+ "Invalid option `pattern` given for `#{name}`, " \
315
+ "`pattern` is only allowed for :string type"
281
316
  end
282
317
 
283
318
  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.6"
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
 
@@ -50,6 +50,71 @@ module InheritanceSpec
50
50
  root "child"
51
51
  end
52
52
  end
53
+
54
+ class Reference < Lutaml::Model::Serializable
55
+ end
56
+
57
+ class FirstRefMapper < Reference
58
+ attribute :name, :string
59
+ attribute :id, :string
60
+
61
+ key_value do
62
+ map "name", to: :name
63
+ map "id", to: :id
64
+ end
65
+ end
66
+
67
+ class SecondRefMapper < Reference
68
+ attribute :name, :string
69
+ attribute :desc, :string
70
+
71
+ key_value do
72
+ map "name", to: :name
73
+ map "desc", to: :desc
74
+ end
75
+ end
76
+
77
+ class FirstRef
78
+ attr_accessor :name, :id
79
+
80
+ def initialize(id:, name:)
81
+ @id = id
82
+ @name = name
83
+ end
84
+ end
85
+
86
+ class SecondRef
87
+ attr_accessor :name, :desc
88
+
89
+ def initialize(desc:, name:)
90
+ @desc = desc
91
+ @name = name
92
+ end
93
+ end
94
+
95
+ class FirstRefMapperWithCustomModel < Reference
96
+ model FirstRef
97
+
98
+ attribute :name, :string
99
+ attribute :id, :string
100
+
101
+ key_value do
102
+ map "name", to: :name
103
+ map "id", to: :id
104
+ end
105
+ end
106
+
107
+ class SecondRefMapperWithCustomModel < Reference
108
+ model SecondRef
109
+
110
+ attribute :name, :string
111
+ attribute :desc, :string
112
+
113
+ key_value do
114
+ map "name", to: :name
115
+ map "desc", to: :desc
116
+ end
117
+ end
53
118
  end
54
119
 
55
120
  RSpec.describe "Inheritance" do
@@ -114,4 +179,88 @@ RSpec.describe "Inheritance" do
114
179
  expect(parsed.to_xml).to eq(xml)
115
180
  end
116
181
  end
182
+
183
+ context "when parent class is given in type" do
184
+ before do
185
+ test_class = Class.new(Lutaml::Model::Serializable) do
186
+ attribute :klass, InheritanceSpec::Reference
187
+
188
+ key_value do
189
+ map "klass", to: :klass
190
+ end
191
+ end
192
+
193
+ stub_const("TestClass", test_class)
194
+ end
195
+
196
+ context "without custom models" do
197
+ let(:first_ref_mapper) do
198
+ InheritanceSpec::FirstRefMapper.new(
199
+ name: "first_mapper",
200
+ id: "one",
201
+ )
202
+ end
203
+
204
+ let(:first_ref_mapper_yaml) do
205
+ <<~YAML
206
+ ---
207
+ klass:
208
+ name: first_mapper
209
+ id: one
210
+ YAML
211
+ end
212
+
213
+ let(:second_ref_mapper) do
214
+ InheritanceSpec::SecondRefMapper.new(
215
+ name: "second_mapper",
216
+ desc: "second mapper",
217
+ )
218
+ end
219
+
220
+ let(:second_ref_mapper_yaml) do
221
+ <<~YAML
222
+ ---
223
+ klass:
224
+ name: second_mapper
225
+ desc: second mapper
226
+ YAML
227
+ end
228
+
229
+ it "outputs correct yaml for first_ref_mapper class" do
230
+ expect(TestClass.new(klass: first_ref_mapper).to_yaml)
231
+ .to eq(first_ref_mapper_yaml)
232
+ end
233
+
234
+ it "outputs correct yaml for second_ref_mapper class" do
235
+ expect(TestClass.new(klass: second_ref_mapper).to_yaml)
236
+ .to eq(second_ref_mapper_yaml)
237
+ end
238
+ end
239
+
240
+ context "when not using custom models" do
241
+ let(:first_ref) do
242
+ InheritanceSpec::FirstRef.new(
243
+ name: "first",
244
+ id: "one",
245
+ )
246
+ end
247
+
248
+ let(:second_ref) do
249
+ InheritanceSpec::SecondRef.new(
250
+ name: "second",
251
+ desc: "second",
252
+ )
253
+ end
254
+
255
+ it "outputs correct yaml for first_ref class" do
256
+ expect { TestClass.new(klass: first_ref).to_yaml }
257
+ .to raise_error(Lutaml::Model::IncorrectModelError)
258
+ end
259
+
260
+ it "outputs correct yaml for second_ref class" do
261
+ expect { TestClass.new(klass: second_ref).to_yaml }
262
+ .to raise_error(Lutaml::Model::IncorrectModelError)
263
+ end
264
+ end
265
+ end
117
266
  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
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.6
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