shale 0.5.0 → 0.6.0

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: aaa3043e612d332fab30ff87ecab722e3812e7769eba982db27c38f74c205ce1
4
- data.tar.gz: bb5cdb963bce5756c48edb37d28bddc40a816901e87b4714ea512f07f8ec4c28
3
+ metadata.gz: 243b99a073ac189f66ae0fbfedd94a5715399de731d0e0fd1d970dad749d27a0
4
+ data.tar.gz: d2f57de2427574d710d6a6fe9ba35868fd162214ce8aa2092ddfb3fe1f5e7c3d
5
5
  SHA512:
6
- metadata.gz: ed1f1ffc88cabb5f9403ff957c02efc4565aee73b9a8a1fd2297ff13bfbb6bdf0e94664b865f8d2d812ddd66fe97d154e34982eea3318ba01c6d16b4a6922c70
7
- data.tar.gz: d41392de36bcace9efee12a869f49beffba235ae09e09f33c6973159b96320e3f657a6edd4d634e53da430f8127e74fa1025fa2f5c75dad2e7568abe6a0794ce
6
+ metadata.gz: 9de1be5eccbb647f05b6573d783491f858fb56614f09c7c625e7fa922c7b89b5d38863a57defbe506d42f0b542b326e4ca61ecf308b94f7b35b4056751e0299e
7
+ data.tar.gz: a1e29fc097c130c55e45837af198e3cd4f7d3ad6a94b17d032eb31b8065f7ea013682836256d12127480965ed3733079e5badd7a8b0060e3bc60f00734fcc215
data/CHANGELOG.md CHANGED
@@ -1,3 +1,14 @@
1
+ ## [0.6.0] - 2022-07-05
2
+
3
+ ### Added
4
+ - Support for TOML
5
+ - Support for CDATA nodes in XML documents
6
+ - Support for using custom models
7
+
8
+ ### Fixed
9
+ - Allow to map XML content using methods
10
+ - Prevent adding default mapping after mapping block was declared
11
+
1
12
  ## [0.5.0] - 2022-06-28
2
13
 
3
14
  ### Added
data/README.md CHANGED
@@ -1,18 +1,18 @@
1
1
  # Shale
2
2
 
3
- Shale is a Ruby object mapper and serializer for JSON, YAML and XML.
4
- It allows you to parse JSON, YAML and XML data and convert it into Ruby data structures,
5
- as well as serialize data structures into JSON, YAML or XML.
3
+ Shale is a Ruby object mapper and serializer for JSON, YAML, TOML and XML.
4
+ It allows you to parse JSON, YAML, TOML and XML data and convert it into Ruby data structures,
5
+ as well as serialize data structures into JSON, YAML, TOML or XML.
6
6
 
7
7
  Documentation with interactive examples is available at [Shale website](https://www.shalerb.org)
8
8
 
9
9
  ## Features
10
10
 
11
- * Convert JSON, YAML and XML to Ruby data model
12
- * Convert Ruby data model to JSON, YAML and XML
11
+ * Convert JSON, YAML, TOML and XML to Ruby data model
12
+ * Convert Ruby data model to JSON, YAML, TOML and XML
13
13
  * Generate JSON and XML Schema from Ruby models
14
14
  * Compile JSON and XML Schema into Ruby models
15
- * Out of the box support for JSON, YAML, Nokogiri, REXML and Ox parsers
15
+ * Out of the box support for JSON, YAML, toml-rb, Nokogiri, REXML and Ox parsers
16
16
  * Support for custom adapters
17
17
 
18
18
  ## Installation
@@ -45,17 +45,21 @@ $ gem install shale
45
45
  * [Converting object to JSON](#converting-object-to-json)
46
46
  * [Converting YAML to object](#converting-yaml-to-object)
47
47
  * [Converting object to YAML](#converting-object-to-yaml)
48
+ * [Converting TOML to object](#converting-toml-to-object)
49
+ * [Converting object to TOML](#converting-object-to-toml)
48
50
  * [Converting Hash to object](#converting-hash-to-object)
49
51
  * [Converting object to Hash](#converting-object-to-hash)
50
52
  * [Converting XML to object](#converting-xml-to-object)
51
53
  * [Converting object to XML](#converting-object-to-xml)
52
54
  * [Mapping JSON keys to object attributes](#mapping-json-keys-to-object-attributes)
53
55
  * [Mapping YAML keys to object attributes](#mapping-yaml-keys-to-object-attributes)
56
+ * [Mapping TOML keys to object attributes](#mapping-toml-keys-to-object-attributes)
54
57
  * [Mapping Hash keys to object attributes](#mapping-hash-keys-to-object-attributes)
55
58
  * [Mapping XML elements and attributes to object attributes](#mapping-xml-elements-and-attributes-to-object-attributes)
56
59
  * [Using XML namespaces](#using-xml-namespaces)
57
60
  * [Using methods to extract and generate data](#using-methods-to-extract-and-generate-data)
58
61
  * [Pretty printing and XML declaration](#pretty-printing-and-xml-declaration)
62
+ * [Using custom models](#using-custom-models)
59
63
  * [Supported types](#supported-types)
60
64
  * [Writing your own type](#writing-your-own-type)
61
65
  * [Adapters](#adapters)
@@ -195,6 +199,62 @@ person.to_yaml
195
199
  # zip: E1 6AN
196
200
  ```
197
201
 
202
+ ### Converting TOML to object
203
+
204
+ To use TOML with Shale you have to set adapter you want to use.
205
+ Shale comes with adapter for [toml-rb](https://github.com/emancu/toml-rb).
206
+ For details see [Adapters](#adapters) section.
207
+
208
+ To set it, first make sure toml-rb gem is installed:
209
+
210
+ ```
211
+ $ gem install shale
212
+ ```
213
+
214
+ then setup adapter:
215
+
216
+ ```ruby
217
+ require 'shale/adapter/toml_rb'
218
+ Shale.toml_adapter = Shale::Adapter::TomlRB
219
+ ```
220
+
221
+ Now you can use TOML with Shale:
222
+
223
+ ```ruby
224
+ person = Person.from_toml(<<~DATA)
225
+ first_name = "John"
226
+ last_name = "Doe"
227
+ age = 50
228
+ married = false
229
+ hobbies = ["Singing", "Dancing"]
230
+ [address]
231
+ city = "London"
232
+ street = "Oxford Street"
233
+ zip = "E1 6AN"
234
+ DATA
235
+ ```
236
+
237
+ ### Converting object to TOML
238
+
239
+ ```ruby
240
+ person.to_toml
241
+
242
+ # =>
243
+ #
244
+ # ---
245
+ # first_name: John
246
+ # last_name: Doe
247
+ # age: 50
248
+ # married: false
249
+ # hobbies:
250
+ # - Singing
251
+ # - Dancing
252
+ # address:
253
+ # city: London
254
+ # street: Oxford Street
255
+ # zip: E1 6AN
256
+ ```
257
+
198
258
  ### Converting Hash to object
199
259
 
200
260
  ```ruby
@@ -240,6 +300,8 @@ require 'shale/adapter/rexml'
240
300
  Shale.xml_adapter = Shale::Adapter::REXML
241
301
  ```
242
302
 
303
+ Now you can use XML with Shale:
304
+
243
305
  ```ruby
244
306
  person = Person.from_xml(<<~DATA)
245
307
  <person>
@@ -284,6 +346,8 @@ person.to_xml
284
346
 
285
347
  By default keys are named the same as attributes. To use custom keys use:
286
348
 
349
+ :warning: **Declaring custom mapping removes default mapping for given format!**
350
+
287
351
  ```ruby
288
352
  class Person < Shale::Mapper
289
353
  attribute :first_name, Shale::Type::String
@@ -310,6 +374,20 @@ class Person < Shale::Mapper
310
374
  end
311
375
  ```
312
376
 
377
+ ### Mapping TOML keys to object attributes
378
+
379
+ ```ruby
380
+ class Person < Shale::Mapper
381
+ attribute :first_name, Shale::Type::String
382
+ attribute :last_name, Shale::Type::String
383
+
384
+ toml do
385
+ map 'firstName', to: :first_name
386
+ map 'lastName', to: :last_name
387
+ end
388
+ end
389
+ ```
390
+
313
391
  ### Mapping Hash keys to object attributes
314
392
 
315
393
  ```ruby
@@ -380,6 +458,37 @@ DATA
380
458
  - `map_attribute` - map element's attribute to attribute
381
459
  - `map_content` - map first text node to attribute
382
460
 
461
+ You can use `cdata: true` option on `map_element` and `map_content` to handle CDATA nodes:
462
+
463
+ ```ruby
464
+ class Address < Shale::Mapper
465
+ attribute :content, Shale::Type::String
466
+
467
+ xml do
468
+ map_content to: :content, cdata: true
469
+ end
470
+ end
471
+
472
+ class Person < Shale::Mapper
473
+ attribute :first_name, Shale::Type::String
474
+ attribute :address, Address
475
+
476
+ xml do
477
+ root 'Person'
478
+
479
+ map_element 'FirstName', to: :first_name, cdata: true
480
+ map_element 'Address', to: :address
481
+ end
482
+ end
483
+
484
+ person = Person.from_xml(<<~DATA)
485
+ <Person>
486
+ <FirstName><![CDATA[John]]></FirstName>
487
+ <Address><![CDATA[Oxford Street]]></Address>
488
+ </person>
489
+ DATA
490
+ ```
491
+
383
492
  ### Using XML namespaces
384
493
 
385
494
  To map namespaced elements and attributes use `namespace` and `prefix` properties on
@@ -469,42 +578,42 @@ class Person < Shale::Mapper
469
578
  map_element 'Address', using: { from: :address_from_xml, to: :address_to_xml }
470
579
  end
471
580
 
472
- def hobbies_from_json(value)
473
- self.hobbies = value.split(',').map(&:strip)
581
+ def hobbies_from_json(model, value)
582
+ model.hobbies = value.split(',').map(&:strip)
474
583
  end
475
584
 
476
- def hobbies_to_json
477
- hobbies.join(', ')
585
+ def hobbies_to_json(model)
586
+ model.hobbies.join(', ')
478
587
  end
479
588
 
480
- def address_from_json(value)
481
- self.street = value['street']
482
- self.city = value['city']
589
+ def address_from_json(model, value)
590
+ model.street = value['street']
591
+ model.city = value['city']
483
592
  end
484
593
 
485
- def address_to_json
486
- { 'street' => street, 'city' => city }
594
+ def address_to_json(model)
595
+ { 'street' => model.street, 'city' => model.city }
487
596
  end
488
597
 
489
- def hobbies_from_xml(value)
490
- self.hobbies = value.split(',').map(&:strip)
598
+ def hobbies_from_xml(model, value)
599
+ model.hobbies = value.split(',').map(&:strip)
491
600
  end
492
601
 
493
- def hobbies_to_xml(element, doc)
494
- doc.add_attribute(element, 'hobbies', hobbies.join(', '))
602
+ def hobbies_to_xml(model, element, doc)
603
+ doc.add_attribute(element, 'hobbies', model.hobbies.join(', '))
495
604
  end
496
605
 
497
- def address_from_xml(node)
498
- self.street = node.children.find { |e| e.name == 'Street' }.text
499
- self.city = node.children.find { |e| e.name == 'City' }.text
606
+ def address_from_xml(model, node)
607
+ model.street = node.children.find { |e| e.name == 'Street' }.text
608
+ model.city = node.children.find { |e| e.name == 'City' }.text
500
609
  end
501
610
 
502
- def address_to_xml(parent, doc)
611
+ def address_to_xml(model, parent, doc)
503
612
  street_element = doc.create_element('Street')
504
- doc.add_text(street_element, street.to_s)
613
+ doc.add_text(street_element, model.street.to_s)
505
614
 
506
615
  city_element = doc.create_element('City')
507
- doc.add_text(city_element, city.to_s)
616
+ doc.add_text(city_element, model.city.to_s)
508
617
 
509
618
  address_element = doc.create_element('Address')
510
619
  doc.add_element(address_element, street_element)
@@ -529,7 +638,7 @@ person = Person.from_xml(<<~DATA)
529
638
  <Street>Oxford Street</Street>
530
639
  <City>London</City>
531
640
  </Address>
532
- </person>
641
+ </Person>
533
642
  DATA
534
643
 
535
644
  # =>
@@ -570,6 +679,67 @@ person.to_xml(:pretty, :declaration)
570
679
  # </Person>
571
680
  ```
572
681
 
682
+ ### Using custom models
683
+
684
+ By default Shale combines mapper and model into one class. If you want to use your own classes
685
+ as models you can do it by using `model` directive on the mapper:
686
+
687
+ ```ruby
688
+ class Address
689
+ attr_accessor :street, :city
690
+ end
691
+
692
+ class Person
693
+ attr_accessor :first_name, :last_name, :address
694
+ end
695
+
696
+ class AddressMapper < Shale::Mapper
697
+ model Address
698
+
699
+ attribute :street, Shale::Type::String
700
+ attribute :city, Shale::Type::String
701
+ end
702
+
703
+ class PersonMapper < Shale::Mapper
704
+ model Person
705
+
706
+ attribute :first_name, Shale::Type::String
707
+ attribute :last_name, Shale::Type::String
708
+ attribute :address, AddressMapper
709
+ end
710
+
711
+ person = PersonMapper.from_json(<<~DATA)
712
+ {
713
+ "first_name": "John",
714
+ "last_name": "Doe",
715
+ "address": {
716
+ "street": "Oxford Street",
717
+ "city": "London"
718
+ }
719
+ }
720
+ DATA
721
+
722
+ # =>
723
+ #
724
+ # #<Person:0x0000000113d7a488
725
+ # @first_name="John",
726
+ # @last_name="Doe",
727
+ # @address=#<Address:0x0000000113d7a140 @street="Oxford Street", @city="London">>
728
+
729
+ PersonMapper.to_json(person, :pretty)
730
+
731
+ # =>
732
+ #
733
+ # {
734
+ # "first_name": "John",
735
+ # "last_name": "Doe",
736
+ # "address": {
737
+ # "street": "Oxford Street",
738
+ # "city": "London"
739
+ # }
740
+ # }
741
+ ```
742
+
573
743
  ### Supported types
574
744
 
575
745
  Shale supports these types out of the box:
@@ -598,9 +768,9 @@ end
598
768
  ### Adapters
599
769
 
600
770
  Shale uses adapters for parsing and generating documents.
601
- By default Ruby's standard JSON and YAML parsers are used for handling JSON and YAML documents.
771
+ By default Ruby's standard JSON, YAML parsers are used for handling JSON and YAML documents.
602
772
 
603
- You can change it by providing your own adapter. For JSON and YAML, adapter must implement
773
+ You can change it by providing your own adapter. For JSON, YAML and TOML, adapter must implement
604
774
  `.load` and `.dump` class methods.
605
775
 
606
776
  ```ruby
@@ -611,6 +781,16 @@ Shale.json_adapter = MultiJson
611
781
  Shale.yaml_adapter = MyYamlAdapter
612
782
  ```
613
783
 
784
+ To handle TOML documents you have to set TOML adapter.
785
+ Shale provides adapter for `toml-rb` TOML parser:
786
+
787
+ ```ruby
788
+ require 'shale'
789
+
790
+ require 'shale/adapter/toml_rb'
791
+ Shale.toml_adapter = Shale::Adapter::TomlRB
792
+ ```
793
+
614
794
  To handle XML documents you have to explicitly set XML adapter.
615
795
  Shale provides adapters for most popular Ruby XML parsers:
616
796
 
@@ -41,6 +41,16 @@ module Shale
41
41
  ::Nokogiri::XML::Element.new(name, @doc)
42
42
  end
43
43
 
44
+ # Create CDATA node and add it to parent
45
+ #
46
+ # @param [String] text
47
+ # @param [::Nokogiri::XML::Element] parent
48
+ #
49
+ # @api private
50
+ def create_cdata(text, parent)
51
+ parent.add_child(::Nokogiri::XML::CDATA.new(@doc, text))
52
+ end
53
+
44
54
  # Add XML namespace to document
45
55
  #
46
56
  # @param [String] prefix
@@ -89,7 +89,7 @@ module Shale
89
89
  first = @node
90
90
  .children
91
91
  .to_a
92
- .filter(&:text?)
92
+ .filter { |e| e.text? || e.cdata? }
93
93
  .first
94
94
 
95
95
  first&.text
@@ -32,6 +32,16 @@ module Shale
32
32
  ::Ox::Element.new(name)
33
33
  end
34
34
 
35
+ # Create CDATA node and add it to parent
36
+ #
37
+ # @param [String] text
38
+ # @param [::Ox::Element] parent
39
+ #
40
+ # @api private
41
+ def create_cdata(text, parent)
42
+ parent << ::Ox::CData.new(text)
43
+ end
44
+
35
45
  # Add XML namespace to document
36
46
  #
37
47
  # Ox doesn't support XML namespaces so this method does nothing.
@@ -80,7 +80,16 @@ module Shale
80
80
  #
81
81
  # @api private
82
82
  def text
83
- @node.text
83
+ texts = @node.nodes.map do |e|
84
+ case e
85
+ when ::Ox::CData
86
+ e.value
87
+ when ::String
88
+ e
89
+ end
90
+ end
91
+
92
+ texts.compact.first
84
93
  end
85
94
  end
86
95
  end
@@ -42,6 +42,16 @@ module Shale
42
42
  ::REXML::Element.new(name, nil, attribute_quote: :quote)
43
43
  end
44
44
 
45
+ # Create CDATA node and add it to parent
46
+ #
47
+ # @param [String] text
48
+ # @param [::REXML::Element] parent
49
+ #
50
+ # @api private
51
+ def create_cdata(text, parent)
52
+ ::REXML::CData.new(text, true, parent)
53
+ end
54
+
45
55
  # Add XML namespace to document
46
56
  #
47
57
  # @param [String] prefix
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'toml-rb'
4
+
5
+ module Shale
6
+ module Adapter
7
+ # TOML adapter
8
+ #
9
+ # @api public
10
+ class TomlRB
11
+ # Parse TOML into Hash
12
+ #
13
+ # @param [String] toml TOML document
14
+ #
15
+ # @return [Hash]
16
+ #
17
+ # @api private
18
+ def self.load(toml)
19
+ ::TomlRB.parse(toml)
20
+ end
21
+
22
+ # Serialize Hash into TOML
23
+ #
24
+ # @param [Hash] obj Hash object
25
+ #
26
+ # @return [String]
27
+ #
28
+ # @api private
29
+ def self.dump(obj)
30
+ ::TomlRB.dump(obj)
31
+ end
32
+ end
33
+ end
34
+ end
data/lib/shale/error.rb CHANGED
@@ -1,9 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Shale
4
- # Error message displayed when adapter is not set
4
+ # Error message displayed when TOML adapter is not set
5
5
  # @api private
6
- ADAPTER_NOT_SET_MESSAGE = <<~MSG
6
+ TOML_ADAPTER_NOT_SET_MESSAGE = <<~MSG
7
+ TOML Adapter is not set.
8
+ To use Shale with TOML documents you have to install parser and set adapter.
9
+
10
+ # Make sure toml-rb is installed eg. execute: gem install toml-rb
11
+ require 'shale/adapter/toml_rb'
12
+ Shale.toml_adapter = Shale::Adapter::TomlRB
13
+ MSG
14
+
15
+ # Error message displayed when XML adapter is not set
16
+ # @api private
17
+ XML_ADAPTER_NOT_SET_MESSAGE = <<~MSG
7
18
  XML Adapter is not set.
8
19
  To use Shale with XML documents you have to install parser and set adapter.
9
20
 
@@ -52,6 +63,12 @@ module Shale
52
63
  end
53
64
  end
54
65
 
66
+ # Error for passing incorrect model type
67
+ #
68
+ # @api private
69
+ class IncorrectModelError < StandardError
70
+ end
71
+
55
72
  # Error for passing incorrect arguments to map functions
56
73
  #
57
74
  # @api private
data/lib/shale/mapper.rb CHANGED
@@ -43,10 +43,12 @@ module Shale
43
43
  #
44
44
  # @api public
45
45
  class Mapper < Type::Complex
46
+ @model = nil
46
47
  @attributes = {}
47
48
  @hash_mapping = Mapping::Dict.new
48
49
  @json_mapping = Mapping::Dict.new
49
50
  @yaml_mapping = Mapping::Dict.new
51
+ @toml_mapping = Mapping::Dict.new
50
52
  @xml_mapping = Mapping::Xml.new
51
53
 
52
54
  class << self
@@ -78,6 +80,13 @@ module Shale
78
80
  # @api public
79
81
  attr_reader :yaml_mapping
80
82
 
83
+ # Return TOML mapping object
84
+ #
85
+ # @return [Shale::Mapping::Dict]
86
+ #
87
+ # @api public
88
+ attr_reader :toml_mapping
89
+
81
90
  # Return XML mapping object
82
91
  #
83
92
  # @return [Shale::Mapping::XML]
@@ -88,16 +97,20 @@ module Shale
88
97
  # @api private
89
98
  def inherited(subclass)
90
99
  super
100
+
101
+ subclass.instance_variable_set('@model', subclass)
91
102
  subclass.instance_variable_set('@attributes', @attributes.dup)
92
103
 
93
104
  subclass.instance_variable_set('@__hash_mapping_init', @hash_mapping.dup)
94
105
  subclass.instance_variable_set('@__json_mapping_init', @json_mapping.dup)
95
106
  subclass.instance_variable_set('@__yaml_mapping_init', @yaml_mapping.dup)
107
+ subclass.instance_variable_set('@__toml_mapping_init', @toml_mapping.dup)
96
108
  subclass.instance_variable_set('@__xml_mapping_init', @xml_mapping.dup)
97
109
 
98
110
  subclass.instance_variable_set('@hash_mapping', @hash_mapping.dup)
99
111
  subclass.instance_variable_set('@json_mapping', @json_mapping.dup)
100
112
  subclass.instance_variable_set('@yaml_mapping', @yaml_mapping.dup)
113
+ subclass.instance_variable_set('@toml_mapping', @toml_mapping.dup)
101
114
 
102
115
  xml_mapping = @xml_mapping.dup
103
116
  xml_mapping.root(Utils.underscore(subclass.name || ''))
@@ -105,6 +118,15 @@ module Shale
105
118
  subclass.instance_variable_set('@xml_mapping', xml_mapping.dup)
106
119
  end
107
120
 
121
+ def model(klass = nil)
122
+ if klass
123
+ @model = klass
124
+ xml_mapping.root(Utils.underscore(@model.name))
125
+ else
126
+ @model
127
+ end
128
+ end
129
+
108
130
  # Define attribute on class
109
131
  #
110
132
  # @param [Symbol] name Name of the attribute
@@ -143,10 +165,11 @@ module Shale
143
165
 
144
166
  @attributes[name] = Attribute.new(name, type, collection, default)
145
167
 
146
- @hash_mapping.map(name.to_s, to: name)
147
- @json_mapping.map(name.to_s, to: name)
148
- @yaml_mapping.map(name.to_s, to: name)
149
- @xml_mapping.map_element(name.to_s, to: name)
168
+ @hash_mapping.map(name.to_s, to: name) unless @hash_mapping.finalized?
169
+ @json_mapping.map(name.to_s, to: name) unless @json_mapping.finalized?
170
+ @yaml_mapping.map(name.to_s, to: name) unless @yaml_mapping.finalized?
171
+ @toml_mapping.map(name.to_s, to: name) unless @toml_mapping.finalized?
172
+ @xml_mapping.map_element(name.to_s, to: name) unless @xml_mapping.finalized?
150
173
 
151
174
  class_eval(<<-RUBY, __FILE__, __LINE__ + 1)
152
175
  attr_reader :#{name}
@@ -168,7 +191,7 @@ module Shale
168
191
  # attribute :age, Shale::Type::Integer
169
192
  #
170
193
  # hsh do
171
- # map 'firatName', to: :first_name
194
+ # map 'firstName', to: :first_name
172
195
  # map 'lastName', to: :last_name
173
196
  # map 'age', to: :age
174
197
  # end
@@ -177,6 +200,7 @@ module Shale
177
200
  # @api public
178
201
  def hsh(&block)
179
202
  @hash_mapping = @__hash_mapping_init.dup
203
+ @hash_mapping.finalize!
180
204
  @hash_mapping.instance_eval(&block)
181
205
  end
182
206
 
@@ -191,7 +215,7 @@ module Shale
191
215
  # attribute :age, Shale::Type::Integer
192
216
  #
193
217
  # json do
194
- # map 'firatName', to: :first_name
218
+ # map 'firstName', to: :first_name
195
219
  # map 'lastName', to: :last_name
196
220
  # map 'age', to: :age
197
221
  # end
@@ -200,6 +224,7 @@ module Shale
200
224
  # @api public
201
225
  def json(&block)
202
226
  @json_mapping = @__json_mapping_init.dup
227
+ @json_mapping.finalize!
203
228
  @json_mapping.instance_eval(&block)
204
229
  end
205
230
 
@@ -214,7 +239,7 @@ module Shale
214
239
  # attribute :age, Shale::Type::Integer
215
240
  #
216
241
  # yaml do
217
- # map 'firat_name', to: :first_name
242
+ # map 'first_name', to: :first_name
218
243
  # map 'last_name', to: :last_name
219
244
  # map 'age', to: :age
220
245
  # end
@@ -223,9 +248,34 @@ module Shale
223
248
  # @api public
224
249
  def yaml(&block)
225
250
  @yaml_mapping = @__yaml_mapping_init.dup
251
+ @yaml_mapping.finalize!
226
252
  @yaml_mapping.instance_eval(&block)
227
253
  end
228
254
 
255
+ # Define TOML mapping
256
+ #
257
+ # @param [Proc] block
258
+ #
259
+ # @example
260
+ # calss Person < Shale::Mapper
261
+ # attribute :first_name, Shale::Type::String
262
+ # attribute :last_name, Shale::Type::String
263
+ # attribute :age, Shale::Type::Integer
264
+ #
265
+ # toml do
266
+ # map 'first_name', to: :first_name
267
+ # map 'last_name', to: :last_name
268
+ # map 'age', to: :age
269
+ # end
270
+ # end
271
+ #
272
+ # @api public
273
+ def toml(&block)
274
+ @toml_mapping = @__toml_mapping_init.dup
275
+ @toml_mapping.finalize!
276
+ @toml_mapping.instance_eval(&block)
277
+ end
278
+
229
279
  # Define XML mapping
230
280
  #
231
281
  # @param [Proc] block
@@ -247,6 +297,8 @@ module Shale
247
297
  # @api public
248
298
  def xml(&block)
249
299
  @xml_mapping = @__xml_mapping_init.dup
300
+ @xml_mapping.finalize!
301
+ @xml_mapping.root('')
250
302
  @xml_mapping.instance_eval(&block)
251
303
  end
252
304
  end
@@ -16,17 +16,26 @@ module Shale
16
16
  # @api private
17
17
  attr_reader :namespace
18
18
 
19
+ # Return cdata
20
+ #
21
+ # @return [true, false]
22
+ #
23
+ # @api private
24
+ attr_reader :cdata
25
+
19
26
  # Initialize instance
20
27
  #
21
28
  # @param [String] name
22
29
  # @param [Symbol, String] attribute
23
30
  # @param [Hash, nil] methods
24
31
  # @param [Shale::Mapping::XmlNamespace] namespace
32
+ # @param [true, false] cdata
25
33
  #
26
34
  # @api private
27
- def initialize(name:, attribute:, methods:, namespace:)
35
+ def initialize(name:, attribute:, methods:, namespace:, cdata:)
28
36
  super(name: name, attribute: attribute, methods: methods)
29
37
  @namespace = namespace
38
+ @cdata = cdata
30
39
  end
31
40
 
32
41
  # Return name with XML prefix
@@ -22,6 +22,7 @@ module Shale
22
22
  def initialize
23
23
  super
24
24
  @keys = {}
25
+ @finalized = false
25
26
  end
26
27
 
27
28
  # Map key to attribute
@@ -38,9 +39,26 @@ module Shale
38
39
  @keys[key] = Descriptor::Dict.new(name: key, attribute: to, methods: using)
39
40
  end
40
41
 
42
+ # Set the "finalized" instance variable to true
43
+ #
44
+ # @api private
45
+ def finalize!
46
+ @finalized = true
47
+ end
48
+
49
+ # Query the "finalized" instance variable
50
+ #
51
+ # @return [truem false]
52
+ #
53
+ # @api private
54
+ def finalized?
55
+ @finalized
56
+ end
57
+
41
58
  # @api private
42
59
  def initialize_dup(other)
43
60
  @keys = other.instance_variable_get('@keys').dup
61
+ @finalized = false
44
62
  super
45
63
  end
46
64
  end
@@ -63,6 +63,7 @@ module Shale
63
63
  @content = nil
64
64
  @root = ''
65
65
  @default_namespace = Descriptor::XmlNamespace.new
66
+ @finalized = false
66
67
  end
67
68
 
68
69
  # Map element to attribute
@@ -76,7 +77,14 @@ module Shale
76
77
  # @raise [IncorrectMappingArgumentsError] when arguments are incorrect
77
78
  #
78
79
  # @api private
79
- def map_element(element, to: nil, using: nil, namespace: :undefined, prefix: :undefined)
80
+ def map_element(
81
+ element,
82
+ to: nil,
83
+ using: nil,
84
+ namespace: :undefined,
85
+ prefix: :undefined,
86
+ cdata: false
87
+ )
80
88
  Validator.validate_arguments(element, to, using)
81
89
  Validator.validate_namespace(element, namespace, prefix)
82
90
 
@@ -94,7 +102,8 @@ module Shale
94
102
  name: element,
95
103
  attribute: to,
96
104
  methods: using,
97
- namespace: Descriptor::XmlNamespace.new(nsp, pfx)
105
+ namespace: Descriptor::XmlNamespace.new(nsp, pfx),
106
+ cdata: cdata
98
107
  )
99
108
  end
100
109
 
@@ -119,7 +128,8 @@ module Shale
119
128
  name: attribute,
120
129
  attribute: to,
121
130
  methods: using,
122
- namespace: Descriptor::XmlNamespace.new(namespace, prefix)
131
+ namespace: Descriptor::XmlNamespace.new(namespace, prefix),
132
+ cdata: false
123
133
  )
124
134
  end
125
135
 
@@ -128,8 +138,16 @@ module Shale
128
138
  # @param [Symbol] to Object's attribute
129
139
  #
130
140
  # @api private
131
- def map_content(to:)
132
- @content = to
141
+ def map_content(to: nil, using: nil, cdata: false)
142
+ Validator.validate_arguments('content', to, using)
143
+
144
+ @content = Descriptor::Xml.new(
145
+ name: nil,
146
+ attribute: to,
147
+ methods: using,
148
+ namespace: nil,
149
+ cdata: cdata
150
+ )
133
151
  end
134
152
 
135
153
  # Set the name for root element
@@ -152,6 +170,22 @@ module Shale
152
170
  @default_namespace.prefix = prefix
153
171
  end
154
172
 
173
+ # Set the "finalized" instance variable to true
174
+ #
175
+ # @api private
176
+ def finalize!
177
+ @finalized = true
178
+ end
179
+
180
+ # Query the "finalized" instance variable
181
+ #
182
+ # @return [truem false]
183
+ #
184
+ # @api private
185
+ def finalized?
186
+ @finalized
187
+ end
188
+
155
189
  # @api private
156
190
  def initialize_dup(other)
157
191
  @elements = other.instance_variable_get('@elements').dup
@@ -159,6 +193,7 @@ module Shale
159
193
  @content = other.instance_variable_get('@content').dup
160
194
  @root = other.instance_variable_get('@root').dup
161
195
  @default_namespace = other.instance_variable_get('@default_namespace').dup
196
+ @finalized = false
162
197
 
163
198
  super
164
199
  end
@@ -200,7 +200,7 @@ module Shale
200
200
  # @api public
201
201
  def as_models(schemas)
202
202
  unless Shale.xml_adapter
203
- raise AdapterError, ADAPTER_NOT_SET_MESSAGE
203
+ raise AdapterError, XML_ADAPTER_NOT_SET_MESSAGE
204
204
  end
205
205
 
206
206
  if Shale.xml_adapter.name == 'Shale::Adapter::Ox'
@@ -11,9 +11,9 @@ module Shale
11
11
  # @api private
12
12
  class Complex < Value
13
13
  class << self
14
- %i[hash json yaml].each do |format|
14
+ %i[hash json yaml toml].each do |format|
15
15
  class_eval(<<-RUBY, __FILE__, __LINE__ + 1)
16
- # Convert Hash to Object using Hash/JSON/YAML mapping
16
+ # Convert Hash to Object using Hash/JSON/YAML/TOML mapping
17
17
  #
18
18
  # @param [Hash] hash Hash to convert
19
19
  #
@@ -21,7 +21,12 @@ module Shale
21
21
  #
22
22
  # @api public
23
23
  def of_#{format}(hash)
24
- instance = new
24
+ instance = model.new
25
+
26
+ attributes
27
+ .values
28
+ .select { |attr| attr.default }
29
+ .each { |attr| instance.send(attr.setter, attr.default.call) }
25
30
 
26
31
  mapping_keys = #{format}_mapping.keys
27
32
 
@@ -30,7 +35,7 @@ module Shale
30
35
  next unless mapping
31
36
 
32
37
  if mapping.method_from
33
- instance.send(mapping.method_from, value)
38
+ new.send(mapping.method_from, instance, value)
34
39
  else
35
40
  attribute = attributes[mapping.attribute]
36
41
  next unless attribute
@@ -44,7 +49,7 @@ module Shale
44
49
  end
45
50
  else
46
51
  val = attribute.type.of_#{format}(value)
47
- instance.send(attribute.setter, val)
52
+ instance.send(attribute.setter, attribute.type.cast(val))
48
53
  end
49
54
  end
50
55
  end
@@ -52,21 +57,28 @@ module Shale
52
57
  instance
53
58
  end
54
59
 
55
- # Convert Object to Hash using Hash/JSON/YAML mapping
60
+ # Convert Object to Hash using Hash/JSON/YAML/TOML mapping
61
+ #
62
+ # @param [any] instance Object to convert
56
63
  #
57
- # @param [Shale::Mapper] instance Object to convert
64
+ # @raise [IncorrectModelError]
58
65
  #
59
66
  # @return [Hash]
60
67
  #
61
68
  # @api public
62
69
  def as_#{format}(instance)
70
+ unless instance.is_a?(model)
71
+ msg = "argument is a '\#{instance.class}' but should be a '\#{model}'"
72
+ raise IncorrectModelError, msg
73
+ end
74
+
63
75
  hash = {}
64
76
 
65
- instance.class.#{format}_mapping.keys.each_value do |mapping|
77
+ #{format}_mapping.keys.each_value do |mapping|
66
78
  if mapping.method_to
67
- hash[mapping.name] = instance.send(mapping.method_to)
79
+ hash[mapping.name] = new.send(mapping.method_to, instance)
68
80
  else
69
- attribute = instance.class.attributes[mapping.attribute]
81
+ attribute = attributes[mapping.attribute]
70
82
  next unless attribute
71
83
 
72
84
  value = instance.send(attribute.name)
@@ -137,6 +149,30 @@ module Shale
137
149
  Shale.yaml_adapter.dump(as_yaml(instance))
138
150
  end
139
151
 
152
+ # Convert TOML to Object
153
+ #
154
+ # @param [String] toml TOML to convert
155
+ #
156
+ # @return [Shale::Mapper]
157
+ #
158
+ # @api public
159
+ def from_toml(toml)
160
+ validate_toml_adapter
161
+ of_toml(Shale.toml_adapter.load(toml))
162
+ end
163
+
164
+ # Convert Object to TOML
165
+ #
166
+ # @param [Shale::Mapper] instance Object to convert
167
+ #
168
+ # @return [String]
169
+ #
170
+ # @api public
171
+ def to_toml(instance)
172
+ validate_toml_adapter
173
+ Shale.toml_adapter.dump(as_toml(instance))
174
+ end
175
+
140
176
  # Convert XML document to Object
141
177
  #
142
178
  # @param [Shale::Adapter::<XML adapter>::Node] xml XML to convert
@@ -145,14 +181,19 @@ module Shale
145
181
  #
146
182
  # @api public
147
183
  def of_xml(element)
148
- instance = new
184
+ instance = model.new
185
+
186
+ attributes
187
+ .values
188
+ .select { |attr| attr.default }
189
+ .each { |attr| instance.send(attr.setter, attr.default.call) }
149
190
 
150
191
  element.attributes.each do |key, value|
151
192
  mapping = xml_mapping.attributes[key.to_s]
152
193
  next unless mapping
153
194
 
154
195
  if mapping.method_from
155
- instance.send(mapping.method_from, value)
196
+ new.send(mapping.method_from, instance, value)
156
197
  else
157
198
  attribute = attributes[mapping.attribute]
158
199
  next unless attribute
@@ -160,16 +201,23 @@ module Shale
160
201
  if attribute.collection?
161
202
  instance.send(attribute.name) << attribute.type.cast(value)
162
203
  else
163
- instance.send(attribute.setter, value)
204
+ instance.send(attribute.setter, attribute.type.cast(value))
164
205
  end
165
206
  end
166
207
  end
167
208
 
168
- if xml_mapping.content
169
- attribute = attributes[xml_mapping.content]
209
+ content_mapping = xml_mapping.content
210
+
211
+ if content_mapping
212
+ if content_mapping.method_from
213
+ new.send(content_mapping.method_from, instance, element)
214
+ else
215
+ attribute = attributes[content_mapping.attribute]
170
216
 
171
- if attribute
172
- instance.send(attribute.setter, attribute.type.of_xml(element))
217
+ if attribute
218
+ value = attribute.type.of_xml(element)
219
+ instance.send(attribute.setter, attribute.type.cast(value))
220
+ end
173
221
  end
174
222
  end
175
223
 
@@ -178,16 +226,17 @@ module Shale
178
226
  next unless mapping
179
227
 
180
228
  if mapping.method_from
181
- instance.send(mapping.method_from, node)
229
+ new.send(mapping.method_from, instance, node)
182
230
  else
183
231
  attribute = attributes[mapping.attribute]
184
232
  next unless attribute
185
233
 
234
+ value = attribute.type.of_xml(node)
235
+
186
236
  if attribute.collection?
187
- value = attribute.type.of_xml(node)
188
237
  instance.send(attribute.name) << attribute.type.cast(value)
189
238
  else
190
- instance.send(attribute.setter, attribute.type.of_xml(node))
239
+ instance.send(attribute.setter, attribute.type.cast(value))
191
240
  end
192
241
  end
193
242
  end
@@ -211,14 +260,21 @@ module Shale
211
260
 
212
261
  # Convert Object to XML document
213
262
  #
214
- # @param [Shale::Mapper] instance Object to convert
263
+ # @param [any] instance Object to convert
215
264
  # @param [String, nil] node_name XML node name
216
265
  # @param [Shale::Adapter::<xml adapter>::Document, nil] doc Object to convert
217
266
  #
267
+ # @raise [IncorrectModelError]
268
+ #
218
269
  # @return [::REXML::Document, ::Nokogiri::Document, ::Ox::Document]
219
270
  #
220
271
  # @api public
221
- def as_xml(instance, node_name = nil, doc = nil)
272
+ def as_xml(instance, node_name = nil, doc = nil, _cdata = nil)
273
+ unless instance.is_a?(model)
274
+ msg = "argument is a '#{instance.class}' but should be a '#{model}'"
275
+ raise IncorrectModelError, msg
276
+ end
277
+
222
278
  unless doc
223
279
  doc = Shale.xml_adapter.create_document
224
280
  doc.add_element(doc.doc, as_xml(instance, xml_mapping.prefixed_root, doc))
@@ -226,13 +282,17 @@ module Shale
226
282
  end
227
283
 
228
284
  element = doc.create_element(node_name)
229
- doc.add_namespace(xml_mapping.default_namespace.prefix, xml_mapping.default_namespace.name)
285
+
286
+ doc.add_namespace(
287
+ xml_mapping.default_namespace.prefix,
288
+ xml_mapping.default_namespace.name
289
+ )
230
290
 
231
291
  xml_mapping.attributes.each_value do |mapping|
232
292
  if mapping.method_to
233
- instance.send(mapping.method_to, element, doc)
293
+ new.send(mapping.method_to, instance, element, doc)
234
294
  else
235
- attribute = instance.class.attributes[mapping.attribute]
295
+ attribute = attributes[mapping.attribute]
236
296
  next unless attribute
237
297
 
238
298
  value = instance.send(attribute.name)
@@ -243,20 +303,33 @@ module Shale
243
303
  end
244
304
  end
245
305
 
246
- if xml_mapping.content
247
- attribute = instance.class.attributes[xml_mapping.content]
306
+ content_mapping = xml_mapping.content
248
307
 
249
- if attribute
250
- value = instance.send(attribute.name)
251
- doc.add_text(element, value.to_s) if value
308
+ if content_mapping
309
+ if content_mapping.method_to
310
+ new.send(content_mapping.method_to, instance, element, doc)
311
+ else
312
+ attribute = attributes[content_mapping.attribute]
313
+
314
+ if attribute
315
+ value = instance.send(attribute.name)
316
+
317
+ # rubocop:disable Metrics/BlockNesting
318
+ if content_mapping.cdata
319
+ doc.create_cdata(value.to_s, element)
320
+ else
321
+ doc.add_text(element, value.to_s)
322
+ end
323
+ # rubocop:enable Metrics/BlockNesting
324
+ end
252
325
  end
253
326
  end
254
327
 
255
328
  xml_mapping.elements.each_value do |mapping|
256
329
  if mapping.method_to
257
- instance.send(mapping.method_to, element, doc)
330
+ new.send(mapping.method_to, instance, element, doc)
258
331
  else
259
- attribute = instance.class.attributes[mapping.attribute]
332
+ attribute = attributes[mapping.attribute]
260
333
  next unless attribute
261
334
 
262
335
  value = instance.send(attribute.name)
@@ -267,10 +340,12 @@ module Shale
267
340
  if attribute.collection?
268
341
  [*value].each do |v|
269
342
  next if v.nil?
270
- doc.add_element(element, attribute.type.as_xml(v, mapping.prefixed_name, doc))
343
+ child = attribute.type.as_xml(v, mapping.prefixed_name, doc, mapping.cdata)
344
+ doc.add_element(element, child)
271
345
  end
272
346
  else
273
- doc.add_element(element, attribute.type.as_xml(value, mapping.prefixed_name, doc))
347
+ child = attribute.type.as_xml(value, mapping.prefixed_name, doc, mapping.cdata)
348
+ doc.add_element(element, child)
274
349
  end
275
350
  end
276
351
  end
@@ -295,13 +370,22 @@ module Shale
295
370
 
296
371
  private
297
372
 
373
+ # Validate TOML adapter
374
+ #
375
+ # @raise [AdapterError]
376
+ #
377
+ # @api private
378
+ def validate_toml_adapter
379
+ raise AdapterError, TOML_ADAPTER_NOT_SET_MESSAGE unless Shale.toml_adapter
380
+ end
381
+
298
382
  # Validate XML adapter
299
383
  #
300
384
  # @raise [AdapterError]
301
385
  #
302
386
  # @api private
303
387
  def validate_xml_adapter
304
- raise AdapterError, ADAPTER_NOT_SET_MESSAGE unless Shale.xml_adapter
388
+ raise AdapterError, XML_ADAPTER_NOT_SET_MESSAGE unless Shale.xml_adapter
305
389
  end
306
390
  end
307
391
 
@@ -334,6 +418,15 @@ module Shale
334
418
  self.class.to_yaml(self)
335
419
  end
336
420
 
421
+ # Convert Object to TOML
422
+ #
423
+ # @return [String]
424
+ #
425
+ # @api public
426
+ def to_toml
427
+ self.class.to_toml(self)
428
+ end
429
+
337
430
  # Convert Object to XML
338
431
  #
339
432
  # @param [Array<Symbol>] options
@@ -89,6 +89,28 @@ module Shale
89
89
  value
90
90
  end
91
91
 
92
+ # Extract value from TOML document
93
+ #
94
+ # @param [any] value
95
+ #
96
+ # @return [any]
97
+ #
98
+ # @api private
99
+ def of_toml(value)
100
+ value
101
+ end
102
+
103
+ # Convert value to form accepted by TOML document
104
+ #
105
+ # @param [any] value
106
+ #
107
+ # @return [any]
108
+ #
109
+ # @api private
110
+ def as_toml(value)
111
+ value
112
+ end
113
+
92
114
  # Extract value from XML document
93
115
  #
94
116
  # @param [Shale::Adapter::<XML adapter>::Node] value
@@ -116,11 +138,18 @@ module Shale
116
138
  # @param [#to_s] value Value to convert to XML
117
139
  # @param [String] name Name of the element
118
140
  # @param [Shale::Adapter::<XML adapter>::Document] doc Document
141
+ # @param [true, false] cdata
119
142
  #
120
143
  # @api private
121
- def as_xml(value, name, doc)
144
+ def as_xml(value, name, doc, cdata = false)
122
145
  element = doc.create_element(name)
123
- doc.add_text(element, as_xml_value(value))
146
+
147
+ if cdata
148
+ doc.create_cdata(as_xml_value(value), element)
149
+ else
150
+ doc.add_text(element, as_xml_value(value))
151
+ end
152
+
124
153
  element
125
154
  end
126
155
  end
data/lib/shale/version.rb CHANGED
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Shale
4
4
  # @api private
5
- VERSION = '0.5.0'
5
+ VERSION = '0.6.0'
6
6
  end
data/lib/shale.rb CHANGED
@@ -72,6 +72,20 @@ module Shale
72
72
  # @api public
73
73
  attr_writer :yaml_adapter
74
74
 
75
+ # TOML adapter accessor.
76
+ #
77
+ # @param [@see Shale::Adapter::TomlRB] adapter
78
+ #
79
+ # @example setting adapter
80
+ # Shale.toml_adapter = Shale::Adapter::TomlRB
81
+ #
82
+ # @example getting adapter
83
+ # Shale.toml_adapter
84
+ # # => Shale::Adapter::TomlRB
85
+ #
86
+ # @api public
87
+ attr_accessor :toml_adapter
88
+
75
89
  # XML adapter accessor. Available adapters are Shale::Adapter::REXML,
76
90
  # Shale::Adapter::Nokogiri and Shale::Adapter::Ox
77
91
  #
data/shale.gemspec CHANGED
@@ -8,8 +8,8 @@ Gem::Specification.new do |spec|
8
8
  spec.authors = ['Kamil Giszczak']
9
9
  spec.email = ['beerkg@gmail.com']
10
10
 
11
- spec.summary = 'Ruby object mapper and serializer for XML, JSON and YAML.'
12
- spec.description = 'Ruby object mapper and serializer for XML, JSON and YAML.'
11
+ spec.summary = 'Ruby object mapper and serializer for XML, JSON, TOML and YAML.'
12
+ spec.description = 'Ruby object mapper and serializer for XML, JSON, TOML and YAML.'
13
13
  spec.homepage = 'https://shalerb.org'
14
14
  spec.license = 'MIT'
15
15
 
metadata CHANGED
@@ -1,16 +1,16 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: shale
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kamil Giszczak
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2022-06-28 00:00:00.000000000 Z
11
+ date: 2022-07-05 00:00:00.000000000 Z
12
12
  dependencies: []
13
- description: Ruby object mapper and serializer for XML, JSON and YAML.
13
+ description: Ruby object mapper and serializer for XML, JSON, TOML and YAML.
14
14
  email:
15
15
  - beerkg@gmail.com
16
16
  executables:
@@ -33,6 +33,7 @@ files:
33
33
  - lib/shale/adapter/rexml.rb
34
34
  - lib/shale/adapter/rexml/document.rb
35
35
  - lib/shale/adapter/rexml/node.rb
36
+ - lib/shale/adapter/toml_rb.rb
36
37
  - lib/shale/attribute.rb
37
38
  - lib/shale/error.rb
38
39
  - lib/shale/mapper.rb
@@ -117,5 +118,5 @@ requirements: []
117
118
  rubygems_version: 3.3.7
118
119
  signing_key:
119
120
  specification_version: 4
120
- summary: Ruby object mapper and serializer for XML, JSON and YAML.
121
+ summary: Ruby object mapper and serializer for XML, JSON, TOML and YAML.
121
122
  test_files: []