shale 0.6.0 → 0.8.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: 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