shale 0.5.0 → 0.6.0

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: 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: []