shale 0.6.0 → 0.8.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: 243b99a073ac189f66ae0fbfedd94a5715399de731d0e0fd1d970dad749d27a0
4
- data.tar.gz: d2f57de2427574d710d6a6fe9ba35868fd162214ce8aa2092ddfb3fe1f5e7c3d
3
+ metadata.gz: 76c0022b3d2d42be143362c84c6697628781acdce7694fb9bc0f305d3f854964
4
+ data.tar.gz: 0b0b1b060eab81ebfae80afa3c31c809280b66ebd77e413c5908c4bbd09a081a
5
5
  SHA512:
6
- metadata.gz: 9de1be5eccbb647f05b6573d783491f858fb56614f09c7c625e7fa922c7b89b5d38863a57defbe506d42f0b542b326e4ca61ecf308b94f7b35b4056751e0299e
7
- data.tar.gz: a1e29fc097c130c55e45837af198e3cd4f7d3ad6a94b17d032eb31b8065f7ea013682836256d12127480965ed3733079e5badd7a8b0060e3bc60f00734fcc215
6
+ metadata.gz: 43bc9188d1f07c5c367a8ed94e2a3da6b878a7b7b86773d7898697abbd3937d4de6ae0471f9362c72e8a6be2cc5e5100a5206b2f2cc7155ff2ff8784953ef702
7
+ data.tar.gz: 0e610703cc73809a78007785962d9a322e515b88ab143a55adfeaa1a0da5961f14d8768a3dec4ad797070bbab165c238123702cbc671f522f86e2934968ff78b
data/CHANGELOG.md CHANGED
@@ -1,3 +1,30 @@
1
+ ## [0.8.0] - 2022-08-30
2
+
3
+ ### Added
4
+ - Allow to group mappings using `group` block
5
+ - Bring back Ruby 2.6 support
6
+
7
+ ### Changed
8
+ - Use anonymous module for attributes definition.
9
+ It allows to override accessors and `super` works as expected.
10
+
11
+ ## [0.7.1] - 2022-08-12
12
+
13
+ ### Fixed
14
+ - Fix broken handling of Date and Time types
15
+
16
+ ## [0.7.0] - 2022-08-09
17
+
18
+ ### Added
19
+ - `only: []` and `except: []` options that allow to controll what attributes are rendered/parsed
20
+ - `render_nil: true` option that allows to render nil values
21
+ - Allow to pass a context object to extractor/generator methods
22
+
23
+ ### Changed
24
+ - Pass whole document to methods for JSON/YAML/TOML so its behavior is consistent with XML mapping
25
+ - Convert splat arguments to keyword arguments
26
+ - RSpec: enable random spec execution and warnings
27
+
1
28
  ## [0.6.0] - 2022-07-05
2
29
 
3
30
  ### Added
data/LICENSE.txt CHANGED
@@ -1,6 +1,6 @@
1
1
  The MIT License (MIT)
2
2
 
3
- Copyright (c) 2021 TODO: Write your name
3
+ Copyright (c) 2021 Kamil Giszczak
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
data/README.md CHANGED
@@ -12,12 +12,12 @@ Documentation with interactive examples is available at [Shale website](https://
12
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, toml-rb, Nokogiri, REXML and Ox parsers
15
+ * Out of the box support for JSON, YAML, Tomlib, toml-rb, Nokogiri, REXML and Ox parsers
16
16
  * Support for custom adapters
17
17
 
18
18
  ## Installation
19
19
 
20
- Shale supports Ruby (MRI) 2.7+
20
+ Shale supports Ruby (MRI) 2.6+
21
21
 
22
22
  Add this line to your application's Gemfile:
23
23
 
@@ -57,8 +57,9 @@ $ gem install shale
57
57
  * [Mapping Hash keys to object attributes](#mapping-hash-keys-to-object-attributes)
58
58
  * [Mapping XML elements and attributes to object attributes](#mapping-xml-elements-and-attributes-to-object-attributes)
59
59
  * [Using XML namespaces](#using-xml-namespaces)
60
+ * [Rendering nil values](#rendering-nil-values)
60
61
  * [Using methods to extract and generate data](#using-methods-to-extract-and-generate-data)
61
- * [Pretty printing and XML declaration](#pretty-printing-and-xml-declaration)
62
+ * [Additional options](#additional-options)
62
63
  * [Using custom models](#using-custom-models)
63
64
  * [Supported types](#supported-types)
64
65
  * [Writing your own type](#writing-your-own-type)
@@ -202,18 +203,23 @@ person.to_yaml
202
203
  ### Converting TOML to object
203
204
 
204
205
  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
+ Out of the box Shale suports [Tomlib](https://github.com/kgiszczak/tomlib).
207
+ It also comes with adapter for [toml-rb](https://github.com/emancu/toml-rb) if you prefer that.
206
208
  For details see [Adapters](#adapters) section.
207
209
 
208
- To set it, first make sure toml-rb gem is installed:
210
+ To set it, first make sure Tomlib gem is installed:
209
211
 
210
212
  ```
211
- $ gem install shale
213
+ $ gem install tomlib
212
214
  ```
213
215
 
214
216
  then setup adapter:
215
217
 
216
218
  ```ruby
219
+ require 'tomlib'
220
+ Shale.toml_adapter = Tomlib
221
+
222
+ # Alternatively if you'd like to use toml-rb, use:
217
223
  require 'shale/adapter/toml_rb'
218
224
  Shale.toml_adapter = Shale::Adapter::TomlRB
219
225
  ```
@@ -241,18 +247,16 @@ person.to_toml
241
247
 
242
248
  # =>
243
249
  #
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
250
+ # first_name = "John"
251
+ # last_name = "Doe"
252
+ # age = 50
253
+ # married = false
254
+ # hobbies = [ "Singing", "Dancing" ]
255
+ #
256
+ # [address]
257
+ # city = "London"
258
+ # street = "Oxford Street"
259
+ # zip = "E1 6AN"
256
260
  ```
257
261
 
258
262
  ### Converting Hash to object
@@ -555,6 +559,52 @@ person = Person.from_xml(<<~DATA)
555
559
  DATA
556
560
  ```
557
561
 
562
+ ### Rendering nil values
563
+
564
+ By default elements with `nil` value are not rendered. You can change this behavior
565
+ by using `render_nil: true` on a mapping.
566
+
567
+ ```ruby
568
+ class Person < Shale::Mapper
569
+ attribute :first_name, Shale::Type::String
570
+ attribute :last_name, Shale::Type::String
571
+ attribute :age, Shale::Type::Integer
572
+
573
+ json do
574
+ map 'first_name', to: :first_name, render_nil: true
575
+ map 'last_name', to: :last_name, render_nil: false
576
+ map 'age', to: :age, render_nil: true
577
+ end
578
+
579
+ xml do
580
+ root 'person'
581
+
582
+ map_element 'first_name', to: :first_name, render_nil: true
583
+ map_element 'last_name', to: :last_name, render_nil: false
584
+ map_attribute 'age', to: :age, render_nil: true
585
+ end
586
+ end
587
+
588
+ person = Person.new(first_name: nil, last_name: nil, age: nil)
589
+
590
+ puts person.to_json(pretty: true)
591
+
592
+ # =>
593
+ #
594
+ # {
595
+ # "first_name": null,
596
+ # "age": "null"
597
+ # }
598
+
599
+ puts person.to_xml(pretty: true)
600
+
601
+ # =>
602
+ #
603
+ # <person age="">
604
+ # <first_name/>
605
+ # </person>
606
+ ```
607
+
558
608
  ### Using methods to extract and generate data
559
609
 
560
610
  If you need full controll over extracting and generating data from/to document,
@@ -582,8 +632,8 @@ class Person < Shale::Mapper
582
632
  model.hobbies = value.split(',').map(&:strip)
583
633
  end
584
634
 
585
- def hobbies_to_json(model)
586
- model.hobbies.join(', ')
635
+ def hobbies_to_json(model, doc)
636
+ doc['hobbies'] = model.hobbies.join(', ')
587
637
  end
588
638
 
589
639
  def address_from_json(model, value)
@@ -591,8 +641,8 @@ class Person < Shale::Mapper
591
641
  model.city = value['city']
592
642
  end
593
643
 
594
- def address_to_json(model)
595
- { 'street' => model.street, 'city' => model.city }
644
+ def address_to_json(model, doc)
645
+ doc['address'] = { 'street' => model.street, 'city' => model.city }
596
646
  end
597
647
 
598
648
  def hobbies_from_xml(model, value)
@@ -649,12 +699,149 @@ DATA
649
699
  # @city="London">
650
700
  ```
651
701
 
652
- ### Pretty printing and XML declaration
702
+ You can also pass a `context` object that will be available in extractor/generator methods:
653
703
 
654
- If you need formatted output you can pass `:pretty` parameter to `#to_json` and `#to_xml`
704
+ ```ruby
705
+ class Person < Shale::Mapper
706
+ attribute :password, Shale::Type::String
707
+
708
+ json do
709
+ map 'password', using: { from: :password_from_json, to: :password_to_json }
710
+ end
711
+
712
+ def password_from_json(model, value, context)
713
+ if context.admin?
714
+ model.password = value
715
+ else
716
+ model.password = '*****'
717
+ end
718
+ end
719
+
720
+ def password_to_json(model, doc, context)
721
+ if context.admin?
722
+ doc['password'] = model.password
723
+ else
724
+ doc['password'] = '*****'
725
+ end
726
+ end
727
+ end
728
+
729
+ Person.new(password: 'secret').to_json(context: current_user)
730
+ ```
731
+
732
+ If you want to work on multiple elements at a time you can group them using `group` block:
655
733
 
656
734
  ```ruby
657
- person.to_json(:pretty)
735
+ class Person < Shale::Mapper
736
+ attribute :name, Shale::Type::String
737
+
738
+ json do
739
+ group from: :name_from_json, to: :name_to_json do
740
+ map 'first_name'
741
+ map 'last_name'
742
+ end
743
+ end
744
+
745
+ xml do
746
+ group from: :name_from_xml, to: :name_to_xml do
747
+ map_content
748
+ map_element 'first_name'
749
+ map_attribute 'last_name'
750
+ end
751
+ end
752
+
753
+ def name_from_json(model, value)
754
+ model.name = "#{value['first_name']} #{value['last_name']}"
755
+ end
756
+
757
+ def name_to_json(model, doc)
758
+ doc['first_name'] = model.name.split(' ')[0]
759
+ doc['last_name'] = model.name.split(' ')[1]
760
+ end
761
+
762
+ def name_from_xml(model, value)
763
+ # value => { content: ..., attributes: {}, elements: {} }
764
+ end
765
+
766
+ def name_to_xml(model, element, doc)
767
+ # ...
768
+ end
769
+ end
770
+
771
+ Person.from_json(<<~DATA)
772
+ {
773
+ "first_name": "John",
774
+ "last_name": "Doe"
775
+ }
776
+ DATA
777
+
778
+ # => #<Person:0x00007f9bc3086d60 @name="John Doe">
779
+ ```
780
+
781
+ ### Additional options
782
+
783
+ You can control which attributes to render and parse by
784
+ using `only: []` and `except: []` parameters.
785
+
786
+ ```ruby
787
+ # e.g. if you have this model graph:
788
+ person = Person.new(
789
+ first_name: 'John'
790
+ last_name: 'Doe',
791
+ address: Address.new(city: 'London', street: 'Oxford Street')
792
+ )
793
+
794
+ # if you want to render only `first_name` and `address.city` do:
795
+ person.to_json(only: [:first_name, address: [:city]], pretty: true)
796
+
797
+ # =>
798
+ #
799
+ # {
800
+ # "first_name": "John",
801
+ # "address": {
802
+ # "city": "London"
803
+ # }
804
+ # }
805
+
806
+ # and if you don't need an address you can do:
807
+ person.to_json(except: [:address], pretty: true)
808
+
809
+ # =>
810
+ #
811
+ # {
812
+ # "first_name": "John",
813
+ # "last_name": "Doe"
814
+ # }
815
+ ```
816
+
817
+ It works the same for parsing:
818
+
819
+ ```ruby
820
+ # e.g. if you want to parse only `address.city` do:
821
+ Person.from_json(doc, only: [address: [:city]])
822
+
823
+ # =>
824
+ #
825
+ # #<Person:0x0000000113d7a488
826
+ # @first_name=nil,
827
+ # @last_name=nil,
828
+ # @address=#<Address:0x0000000113d7a140 @street=nil, @city="London">>
829
+
830
+ # and if you don't need an `address`:
831
+ Person.from_json(doc, except: [:address])
832
+
833
+ # =>
834
+ #
835
+ # #<Person:0x0000000113d7a488
836
+ # @first_name="John",
837
+ # @last_name="Doe",
838
+ # @address=nil>
839
+ ```
840
+
841
+ If you need formatted output you can pass `pretty: true` parameter to `#to_json` and `#to_xml`
842
+
843
+ ```ruby
844
+ person.to_json(pretty: true)
658
845
 
659
846
  # =>
660
847
  #
@@ -666,10 +853,10 @@ person.to_json(:pretty)
666
853
  # }
667
854
  ```
668
855
 
669
- You can also add an XML declaration by passing `:declaration` to `#to_xml`
856
+ You can also add an XML declaration by passing `declaration: true` to `#to_xml`
670
857
 
671
858
  ```ruby
672
- person.to_xml(:pretty, :declaration)
859
+ person.to_xml(pretty: true, declaration: true)
673
860
 
674
861
  # =>
675
862
  #
@@ -726,7 +913,7 @@ DATA
726
913
  # @last_name="Doe",
727
914
  # @address=#<Address:0x0000000113d7a140 @street="Oxford Street", @city="London">>
728
915
 
729
- PersonMapper.to_json(person, :pretty)
916
+ PersonMapper.to_json(person, pretty: true)
730
917
 
731
918
  # =>
732
919
  #
@@ -781,12 +968,17 @@ Shale.json_adapter = MultiJson
781
968
  Shale.yaml_adapter = MyYamlAdapter
782
969
  ```
783
970
 
784
- To handle TOML documents you have to set TOML adapter.
785
- Shale provides adapter for `toml-rb` TOML parser:
971
+ To handle TOML documents you have to set TOML adapter. Out of the box `Tomlib` is supported.
972
+ Shale also provides adapter for `toml-rb` parser:
786
973
 
787
974
  ```ruby
788
975
  require 'shale'
789
976
 
977
+ # if you want to use Tomlib
978
+ require 'tomlib'
979
+ Shale.toml_adapter = Tomlib
980
+
981
+ # if you want to use toml-rb
790
982
  require 'shale/adapter/toml_rb'
791
983
  Shale.toml_adapter = Shale::Adapter::TomlRB
792
984
  ```
@@ -22,13 +22,13 @@ module Shale
22
22
  # Serialize Hash into JSON
23
23
  #
24
24
  # @param [Hash] obj Hash object
25
- # @param [Array<Symbol>] options
25
+ # @param [true, false] pretty
26
26
  #
27
27
  # @return [String]
28
28
  #
29
29
  # @api private
30
- def self.dump(obj, *options)
31
- if options.include?(:pretty)
30
+ def self.dump(obj, pretty: false)
31
+ if pretty
32
32
  ::JSON.pretty_generate(obj)
33
33
  else
34
34
  ::JSON.generate(obj)
@@ -36,25 +36,26 @@ module Shale
36
36
  # Serialize Nokogiri document into XML
37
37
  #
38
38
  # @param [::Nokogiri::XML::Document] doc Nokogiri document
39
- # @param [Array<Symbol>] options
39
+ # @param [true, false] pretty
40
+ # @param [true, false] declaration
40
41
  #
41
42
  # @return [String]
42
43
  #
43
44
  # @api private
44
- def self.dump(doc, *options)
45
+ def self.dump(doc, pretty: false, declaration: false)
45
46
  save_with = ::Nokogiri::XML::Node::SaveOptions::AS_XML
46
47
 
47
- if options.include?(:pretty)
48
+ if pretty
48
49
  save_with |= ::Nokogiri::XML::Node::SaveOptions::FORMAT
49
50
  end
50
51
 
51
- unless options.include?(:declaration)
52
+ unless declaration
52
53
  save_with |= ::Nokogiri::XML::Node::SaveOptions::NO_DECLARATION
53
54
  end
54
55
 
55
56
  result = doc.to_xml(save_with: save_with)
56
57
 
57
- unless options.include?(:pretty)
58
+ unless pretty
58
59
  result = result.sub(/\n/, '')
59
60
  end
60
61
 
@@ -30,19 +30,20 @@ module Shale
30
30
  # Serialize Ox document into XML
31
31
  #
32
32
  # @param [::Ox::Document, ::Ox::Element] doc Ox document
33
- # @param [Array<Symbol>] options
33
+ # @param [true, false] pretty
34
+ # @param [true, false] declaration
34
35
  #
35
36
  # @return [String]
36
37
  #
37
38
  # @api private
38
- def self.dump(doc, *options)
39
+ def self.dump(doc, pretty: false, declaration: false)
39
40
  opts = { indent: -1, with_xml: false }
40
41
 
41
- if options.include?(:pretty)
42
+ if pretty
42
43
  opts[:indent] = 2
43
44
  end
44
45
 
45
- if options.include?(:declaration)
46
+ if declaration
46
47
  doc[:version] = '1.0'
47
48
  opts[:with_xml] = true
48
49
  end
@@ -70,7 +70,7 @@ module Shale
70
70
  #
71
71
  # @api private
72
72
  def add_attribute(element, name, value)
73
- element.add_attribute(name, value)
73
+ element.add_attribute(name, value || '')
74
74
  end
75
75
 
76
76
  # Add child element to REXML element
@@ -31,19 +31,20 @@ module Shale
31
31
  # Serialize REXML document into XML
32
32
  #
33
33
  # @param [::REXML::Document] doc REXML document
34
- # @param [Array<Symbol>] options
34
+ # @param [true, false] pretty
35
+ # @param [true, false] declaration
35
36
  #
36
37
  # @return [String]
37
38
  #
38
39
  # @api private
39
- def self.dump(doc, *options)
40
- if options.include?(:declaration)
40
+ def self.dump(doc, pretty: false, declaration: false)
41
+ if declaration
41
42
  doc.add(::REXML::XMLDecl.new)
42
43
  end
43
44
 
44
45
  io = StringIO.new
45
46
 
46
- if options.include?(:pretty)
47
+ if pretty
47
48
  formatter = ::REXML::Formatters::Pretty.new
48
49
  formatter.compact = true
49
50
  else
data/lib/shale/error.rb CHANGED
@@ -7,6 +7,11 @@ module Shale
7
7
  TOML Adapter is not set.
8
8
  To use Shale with TOML documents you have to install parser and set adapter.
9
9
 
10
+ # To use Tomlib:
11
+ # Make sure tomlib is installed eg. execute: gem install tomlib
12
+ Shale.toml_adapter = Tomlib
13
+
14
+ # To use toml-rb:
10
15
  # Make sure toml-rb is installed eg. execute: gem install toml-rb
11
16
  require 'shale/adapter/toml_rb'
12
17
  Shale.toml_adapter = Shale::Adapter::TomlRB
@@ -18,16 +23,16 @@ module Shale
18
23
  XML Adapter is not set.
19
24
  To use Shale with XML documents you have to install parser and set adapter.
20
25
 
21
- To use REXML:
26
+ # To use REXML:
22
27
  require 'shale/adapter/rexml'
23
28
  Shale.xml_adapter = Shale::Adapter::REXML
24
29
 
25
- To use Nokogiri:
30
+ # To use Nokogiri:
26
31
  # Make sure Nokogiri is installed eg. execute: gem install nokogiri
27
32
  require 'shale/adapter/nokogiri'
28
33
  Shale.xml_adapter = Shale::Adapter::Nokogiri
29
34
 
30
- To use OX:
35
+ # To use OX:
31
36
  # Make sure Ox is installed eg. execute: gem install ox
32
37
  require 'shale/adapter/ox'
33
38
  Shale.xml_adapter = Shale::Adapter::Ox
data/lib/shale/mapper.rb CHANGED
@@ -98,6 +98,10 @@ module Shale
98
98
  def inherited(subclass)
99
99
  super
100
100
 
101
+ attributes_module = Module.new
102
+ subclass.instance_variable_set('@attributes_module', attributes_module)
103
+ subclass.include(attributes_module)
104
+
101
105
  subclass.instance_variable_set('@model', subclass)
102
106
  subclass.instance_variable_set('@attributes', @attributes.dup)
103
107
 
@@ -171,7 +175,7 @@ module Shale
171
175
  @toml_mapping.map(name.to_s, to: name) unless @toml_mapping.finalized?
172
176
  @xml_mapping.map_element(name.to_s, to: name) unless @xml_mapping.finalized?
173
177
 
174
- class_eval(<<-RUBY, __FILE__, __LINE__ + 1)
178
+ @attributes_module.class_eval(<<-RUBY, __FILE__, __LINE__ + 1)
175
179
  attr_reader :#{name}
176
180
 
177
181
  def #{name}=(val)
@@ -35,22 +35,42 @@ module Shale
35
35
  # @api private
36
36
  attr_reader :method_to
37
37
 
38
+ # Return group name
39
+ #
40
+ # @return [String]
41
+ #
42
+ # @api private
43
+ attr_reader :group
44
+
38
45
  # Initialize instance
39
46
  #
40
47
  # @param [String] name
41
48
  # @param [Symbol, nil] attribute
42
49
  # @param [Hash, nil] methods
50
+ # @param [String, nil] group
51
+ # @param [true, false] render_nil
43
52
  #
44
53
  # @api private
45
- def initialize(name:, attribute:, methods:)
54
+ def initialize(name:, attribute:, methods:, group:, render_nil:)
46
55
  @name = name
47
56
  @attribute = attribute
57
+ @group = group
58
+ @render_nil = render_nil
48
59
 
49
60
  if methods
50
61
  @method_from = methods[:from]
51
62
  @method_to = methods[:to]
52
63
  end
53
64
  end
65
+
66
+ # Check render_nil
67
+ #
68
+ # @return [true, false]
69
+ #
70
+ # @api private
71
+ def render_nil?
72
+ @render_nil == true
73
+ end
54
74
  end
55
75
  end
56
76
  end
@@ -28,12 +28,21 @@ module Shale
28
28
  # @param [String] name
29
29
  # @param [Symbol, String] attribute
30
30
  # @param [Hash, nil] methods
31
+ # @param [String, nil] group
31
32
  # @param [Shale::Mapping::XmlNamespace] namespace
32
33
  # @param [true, false] cdata
34
+ # @param [true, false] render_nil
33
35
  #
34
36
  # @api private
35
- def initialize(name:, attribute:, methods:, namespace:, cdata:)
36
- super(name: name, attribute: attribute, methods: methods)
37
+ def initialize(name:, attribute:, methods:, group:, namespace:, cdata:, render_nil:)
38
+ super(
39
+ name: name,
40
+ attribute: attribute,
41
+ methods: methods,
42
+ group: group,
43
+ render_nil: render_nil
44
+ )
45
+
37
46
  @namespace = namespace
38
47
  @cdata = cdata
39
48
  end
@@ -46,6 +55,15 @@ module Shale
46
55
  def prefixed_name
47
56
  [namespace.prefix, name].compact.join(':')
48
57
  end
58
+
59
+ # Return name with XML namespace
60
+ #
61
+ # @return [String]
62
+ #
63
+ # @api private
64
+ def namespaced_name
65
+ [namespace.name, name].compact.join(':')
66
+ end
49
67
  end
50
68
  end
51
69
  end