shale 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 4c96481848145b9736af4cc830c84c04970042fdc61f7d348aa64099065319ef
4
+ data.tar.gz: 25739ce30a7f4e66040b5dbff394da06a5c578065af0cd0b03bd0a56c7d78cbf
5
+ SHA512:
6
+ metadata.gz: f205c1407868c6469922b7553f9aef3cdb0dc70502981d56eeef5b08703c793213f63c2a898568f372bc4e9519bcbfa771eefd6cfd663746e5b4180d1738c351
7
+ data.tar.gz: 95d7e2c6f88d7d5099c1ff62345509e83faee23a3bdd16e77d8eea05fb061e0eeb76fabbba5dc50d7c15ea675d2d1430570a2ac4134416c3d03d905d4ef59528
data/CHANGELOG.md ADDED
@@ -0,0 +1,3 @@
1
+ ## [0.1.0] - 2021-11-30
2
+
3
+ First public release
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2021 TODO: Write your name
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,392 @@
1
+ # Shale
2
+
3
+ Shale is a object mapper and serializer for JSON, YAML and XML.
4
+
5
+ ## Installation
6
+
7
+ Shale supports Ruby (MRI) 2.6+
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ ```ruby
12
+ gem 'shale'
13
+ ```
14
+
15
+ And then execute:
16
+
17
+ ```
18
+ $ bundle install
19
+ ```
20
+
21
+ Or install it yourself as:
22
+
23
+ ```
24
+ $ gem install shale
25
+ ```
26
+
27
+ ## Usage
28
+
29
+ ### Simple use case
30
+
31
+ ```ruby
32
+ require 'shale'
33
+
34
+ class Address < Shale::Mapper
35
+ attribute :city, Shale::Type::String
36
+ attribute :street, Shale::Type::String
37
+ attribute :zip, Shale::Type::String
38
+ end
39
+
40
+ class Person < Shale::Mapper
41
+ attribute :first_name, Shale::Type::String
42
+ attribute :last_name, Shale::Type::String
43
+ attribute :age, Shale::Type::Date
44
+ attribute :married, Shale::Type::Boolean, default: -> { false }
45
+ attribute :hobbies, Shale::Type::String, collection: true
46
+ attribute :address, Address
47
+ end
48
+ ```
49
+
50
+ - `default: -> { 'value' }` - add a default value to attribute (it must be a proc that returns value)
51
+ - `collection: true` - indicated that a attribute is a collection
52
+
53
+ ### Creating objects
54
+
55
+ ```ruby
56
+ person = Person.new(
57
+ first_name: 'John',
58
+ last_name: 'Doe',
59
+ age: 50,
60
+ hobbies: ['Singing', 'Dancing'],
61
+ address: Address.new(city: 'London', street: 'Oxford Street', zip: 'E1 6AN'),
62
+ )
63
+ ```
64
+
65
+ ### Converting JSON to object
66
+
67
+ ```ruby
68
+ person = Person.from_json(<<~DATA)
69
+ {
70
+ "first_name": "John",
71
+ "last_name": "Doe",
72
+ "age": 50,
73
+ "married": false,
74
+ "hobbies": ["Singing", "Dancing"],
75
+ "address": {
76
+ "city": "London",
77
+ "street": "Oxford Street",
78
+ "zip": "E1 6AN"
79
+ }
80
+ }
81
+ DATA
82
+
83
+ # =>
84
+ #
85
+ # #<Person:0x00007f9bc3086d60
86
+ # @address=
87
+ # #<Address:0x00007f9bc3086748
88
+ # @city="London",
89
+ # @street="Oxford Street",
90
+ # @zip="E1 6AN">,
91
+ # @age=50,
92
+ # @first_name="John",
93
+ # @hobbies=["Singing", "Dancing"],
94
+ # @last_name="Doe",
95
+ # @married=false>
96
+ ```
97
+
98
+ ### Converting object to JSON
99
+
100
+ ```ruby
101
+ person.to_json
102
+ # =>
103
+ #
104
+ # {
105
+ # "first_name": "John",
106
+ # "last_name": "Doe",
107
+ # "age": 50,
108
+ # "married": false,
109
+ # "hobbies": ["Singing", "Dancing"],
110
+ # "address": {
111
+ # "city": "London",
112
+ # "street": "Oxford Street",
113
+ # "zip": "E1 6AN"
114
+ # }
115
+ # }
116
+ ```
117
+
118
+ ### Converting YAML to object
119
+
120
+ ```ruby
121
+ person = Person.from_yaml(<<~DATA)
122
+ first_name: John
123
+ last_name: Doe
124
+ age: 50
125
+ married: false
126
+ hobbies:
127
+ - Singing
128
+ - Dancing
129
+ address:
130
+ city: London
131
+ street: Oxford Street
132
+ zip: E1 6AN
133
+ DATA
134
+ ```
135
+
136
+ ### Converting object to YAML
137
+
138
+ ```ruby
139
+ person.to_yaml
140
+ # =>
141
+ #
142
+ # ---
143
+ # first_name: John
144
+ # last_name: Doe
145
+ # age: 50
146
+ # married: false
147
+ # hobbies:
148
+ # - Singing
149
+ # - Dancing
150
+ # address:
151
+ # city: London
152
+ # street: Oxford Street
153
+ # zip: E1 6AN
154
+ ```
155
+
156
+ ### Converting Hash to object
157
+
158
+ ```ruby
159
+ person = Person.from_hash(
160
+ 'first_name' => 'John',
161
+ 'last_name' => 'Doe',
162
+ 'age' => 50,
163
+ 'married' => false,
164
+ 'hobbies' => ['Singing', 'Dancing'],
165
+ 'address' => {
166
+ 'city'=>'London',
167
+ 'street'=>'Oxford Street',
168
+ 'zip'=>'E1 6AN'
169
+ },
170
+ )
171
+ ```
172
+
173
+ ### Converting object to Hash
174
+
175
+ ```ruby
176
+ person.to_hash
177
+ # =>
178
+ #
179
+ # {
180
+ # "first_name"=>"John",
181
+ # "last_name"=>"Doe",
182
+ # "age"=>50,
183
+ # "married"=>false,
184
+ # "hobbies"=>["Singing", "Dancing"],
185
+ # "address"=>{"city"=>"London", "street"=>"Oxford Street", "zip"=>"E1 6AN"}
186
+ # }
187
+ ```
188
+
189
+ ### Converting XML to object
190
+
191
+ ```ruby
192
+ person = Person.from_xml(<<~DATA)
193
+ <person>
194
+ <first_name>John</first_name>
195
+ <last_name>Doe</last_name>
196
+ <age>50</age>
197
+ <married>false</married>
198
+ <hobbies>Singing</hobbies>
199
+ <hobbies>Dancing</hobbies>
200
+ <address>
201
+ <city>London</city>
202
+ <street>Oxford Street</street>
203
+ <zip>E1 6AN</zip>
204
+ </address>
205
+ </person>
206
+ DATA
207
+ ```
208
+
209
+ ### Converting object to XML
210
+
211
+ ```ruby
212
+ person.to_xml
213
+ # =>
214
+ #
215
+ # <person>
216
+ # <first_name>John</first_name>
217
+ # <last_name>Doe</last_name>
218
+ # <age>50</age>
219
+ # <married>false</married>
220
+ # <hobbies>Singing</hobbies>
221
+ # <hobbies>Dancing</hobbies>
222
+ # <address>
223
+ # <city>London</city>
224
+ # <street>Oxford Street</street>
225
+ # <zip>E1 6AN</zip>
226
+ # </address>
227
+ # </person>
228
+ ```
229
+
230
+ ### Mapping JSON keys to object attributes
231
+
232
+ By default keys are named the same as attributes. To use custom key names use:
233
+
234
+ ```ruby
235
+ class Person < Shale::Mapper
236
+ attribute :first_name, Shale::Type::String
237
+ attribute :last_name, Shale::Type::String
238
+
239
+ json do
240
+ map 'firstName', to: :first_name
241
+ map 'lastName', to: :last_name
242
+ end
243
+ end
244
+ ```
245
+
246
+ ### Mapping YAML keys to object attributes
247
+
248
+ ```ruby
249
+ class Person < Shale::Mapper
250
+ attribute :first_name, Shale::Type::String
251
+ attribute :last_name, Shale::Type::String
252
+
253
+ yaml do
254
+ map 'firstName', to: :first_name
255
+ map 'lastName', to: :last_name
256
+ end
257
+ end
258
+ ```
259
+
260
+ ### Mapping Hash keys to object attributes
261
+
262
+ ```ruby
263
+ class Person < Shale::Mapper
264
+ attribute :first_name, Shale::Type::String
265
+ attribute :last_name, Shale::Type::String
266
+
267
+ hash do
268
+ map 'firstName', to: :first_name
269
+ map 'lastName', to: :last_name
270
+ end
271
+ end
272
+ ```
273
+
274
+ ### Mapping XML elements and attributes to object attributes
275
+
276
+ XML is more complcated format than JSON or YAML. To map elements, attributes and content use:
277
+
278
+ ```ruby
279
+ class Address < Shale::Mapper
280
+ attribute :street, Shale::Type::String
281
+ attribute :city, Shale::Type::String
282
+ attribute :zip, Shale::Type::String
283
+
284
+ xml do
285
+ map_content to: :street
286
+ map_element 'City', to: :city
287
+ map_element 'ZIP', to: :zip
288
+ end
289
+ end
290
+
291
+ class Person < Shale::Mapper
292
+ attribute :first_name, Shale::Type::String
293
+ attribute :last_name, Shale::Type::String
294
+ attribute :age, Shale::Type::Date
295
+ attribute :hobbies, Shale::Type::String, collection: true
296
+ attribute :address, Address
297
+
298
+ xml do
299
+ root 'Person'
300
+
301
+ map_attribute 'age', to: :age
302
+
303
+ map_element 'FirstName', to: :first_name
304
+ map_element 'LastName', to: :last_name
305
+ map_element 'Hobby', to: :hobbies
306
+ map_element 'Address', to: :address
307
+ end
308
+ end
309
+
310
+ person = Person.from_xml(<<~DATA)
311
+ <Person age="50">
312
+ <FirstName>John</FirstName>
313
+ <LastName>Doe</LastName>
314
+ <Hobby>Singing</Hobby>
315
+ <Hobby>Dancing</Hobby>
316
+ <Address>
317
+ Oxford Street
318
+ <City>London</City>
319
+ <ZIP>E1 6AN</ZIP>
320
+ </Address>
321
+ </person>
322
+ DATA
323
+ ```
324
+
325
+ - `root` - name of the root element
326
+ - `map_element` - map content of element to attribute
327
+ - `map_attribute` - map element's attribute to attribute
328
+ - `map_content` - map first text node to attribute
329
+
330
+ ### Supported types
331
+
332
+ Shale supports these types out of the box:
333
+
334
+ - `Shale::Type::Boolean`
335
+ - `Shale::Type::Date`
336
+ - `Shale::Type::Float`
337
+ - `Shale::Type::Integer`
338
+ - `Shale::Type::String`
339
+ - `Shale::Type::Time`
340
+
341
+ ### Writing your own type
342
+
343
+ To add your own type extend it from `Shale::Type::Base` and implement `.cast` class method.
344
+
345
+ ```ruby
346
+ require 'shale/type/base'
347
+
348
+ class MyIntegerType < Shale::Type::Base
349
+ def self.cast(value)
350
+ value.to_i
351
+ end
352
+ end
353
+ ```
354
+
355
+ ### Adapters
356
+
357
+ Shale uses adapters for parsing and generating documents.
358
+ By default Ruby's standard JSON parser is used for handing JSON documents, YAML for YAML and
359
+ REXML for XML.
360
+
361
+ You can change it by providing your own adapter. For JSON and YAML adapter must implement
362
+ `.load` and `.dump` class methods.
363
+
364
+ ```ruby
365
+ require 'shale'
366
+ require 'multi_json'
367
+
368
+ Shale.json_adapter = MultiJson
369
+ Shale.yaml_adapter = MyYamlAdapter
370
+ ```
371
+
372
+ For XML, other than REXML, Shale provides adapters for most popular Ruby XML parsers:
373
+
374
+ ```ruby
375
+ require 'shale'
376
+
377
+ require 'shale/adapter/nokogiri'
378
+ Shale.xml_adapter = Shale::Adapter::Nokogiri
379
+
380
+ # or if you want to use Ox
381
+
382
+ require 'shale/adapter/ox'
383
+ Shale.xml_adapter = Shale::Adapter::Ox
384
+ ```
385
+
386
+ ## Contributing
387
+
388
+ Bug reports and pull requests are welcome on GitHub at https://github.com/kgiszczak/shale.
389
+
390
+ ## License
391
+
392
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module Shale
6
+ module Adapter
7
+ # JSON adapter
8
+ #
9
+ # @api public
10
+ class JSON
11
+ # Parse JSON into Hash
12
+ #
13
+ # @param [String] json JSON document
14
+ #
15
+ # @return [Hash]
16
+ #
17
+ # @api private
18
+ def self.load(json)
19
+ ::JSON.parse(json)
20
+ end
21
+
22
+ # Serialize Hash into JSON
23
+ #
24
+ # @param [Hash] obj Hash object
25
+ #
26
+ # @return [String]
27
+ #
28
+ # @api private
29
+ def self.dump(obj)
30
+ ::JSON.generate(obj)
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,176 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'nokogiri'
4
+
5
+ module Shale
6
+ module Adapter
7
+ # Nokogiri adapter
8
+ #
9
+ # @api public
10
+ module Nokogiri
11
+ # Parse XML into Nokogiri document
12
+ #
13
+ # @param [String] xml XML document
14
+ #
15
+ # @return [::Nokogiri::XML::Document]
16
+ #
17
+ # @api private
18
+ def self.load(xml)
19
+ doc = ::Nokogiri::XML::Document.parse(xml) do |config|
20
+ config.noblanks
21
+ end
22
+
23
+ Node.new(doc.root)
24
+ end
25
+
26
+ # Serialize Nokogiri document into XML
27
+ #
28
+ # @param [::Nokogiri::XML::Document] doc Nokogiri document
29
+ #
30
+ # @return [String]
31
+ #
32
+ # @api private
33
+ def self.dump(doc)
34
+ doc.to_xml
35
+ end
36
+
37
+ # Create Shale::Adapter::Nokogiri::Document instance
38
+ #
39
+ # @api private
40
+ def self.create_document
41
+ Document.new
42
+ end
43
+
44
+ # Wrapper around Nokogiri API
45
+ #
46
+ # @api private
47
+ class Document
48
+ # Return Nokogiri document
49
+ #
50
+ # @return [::Nokogiri::XML::Document]
51
+ #
52
+ # @api private
53
+ attr_reader :doc
54
+
55
+ # Initialize object
56
+ #
57
+ # @api private
58
+ def initialize
59
+ @doc = ::Nokogiri::XML::Document.new
60
+ end
61
+
62
+ # Create Nokogiri element
63
+ #
64
+ # @param [String] name Name of the XML element
65
+ #
66
+ # @return [::Nokogiri::XML::Element]
67
+ #
68
+ # @api private
69
+ def create_element(name)
70
+ ::Nokogiri::XML::Element.new(name, @doc)
71
+ end
72
+
73
+ # Add attribute to Nokogiri element
74
+ #
75
+ # @param [::Nokogiri::XML::Element] element Nokogiri element
76
+ # @param [String] name Name of the XML attribute
77
+ # @param [String] value Value of the XML attribute
78
+ #
79
+ # @api private
80
+ def add_attribute(element, name, value)
81
+ element[name] = value
82
+ end
83
+
84
+ # Add child element to Nokogiri element
85
+ #
86
+ # @param [::Nokogiri::XML::Element] element Nokogiri parent element
87
+ # @param [::Nokogiri::XML::Element] child Nokogiri child element
88
+ #
89
+ # @api private
90
+ def add_element(element, child)
91
+ element.add_child(child)
92
+ end
93
+
94
+ # Add text node to Nokogiri element
95
+ #
96
+ # @param [::Nokogiri::XML::Element] element Nokogiri element
97
+ # @param [String] text Text to add
98
+ #
99
+ # @api private
100
+ def add_text(element, text)
101
+ element.content = text
102
+ end
103
+ end
104
+
105
+ # Wrapper around Nokogiri::XML::Node API
106
+ #
107
+ # @api private
108
+ class Node
109
+ # Initialize object with Nokogiri node
110
+ #
111
+ # @param [::Nokogiri::XML::Node] node Nokogiri node
112
+ #
113
+ # @api private
114
+ def initialize(node)
115
+ @node = node
116
+ end
117
+
118
+ # Return fully qualified name of the node in the format of
119
+ # namespace:name when the node is namespaced or just name when it's not
120
+ #
121
+ # @return [String]
122
+ #
123
+ # @example without namespace
124
+ # node.name # => Bar
125
+ #
126
+ # @example with namespace
127
+ # node.name # => foo:Bar
128
+ #
129
+ # @api private
130
+ def name
131
+ [@node.namespace&.prefix, @node.name].compact.join(':')
132
+ end
133
+
134
+ # Return all attributes associated with the node
135
+ #
136
+ # @return [Hash]
137
+ #
138
+ # @api private
139
+ def attributes
140
+ @node.attribute_nodes.each_with_object({}) do |node, hash|
141
+ name = [node.namespace&.prefix, node.name].compact.join(':')
142
+ hash[name] = node.value
143
+ end
144
+ end
145
+
146
+ # Return node's element children
147
+ #
148
+ # @return [Array<Shale::Adapter::Nokogiri::Node>]
149
+ #
150
+ # @api private
151
+ def children
152
+ @node
153
+ .children
154
+ .to_a
155
+ .filter(&:element?)
156
+ .map { |e| self.class.new(e) }
157
+ end
158
+
159
+ # Return first text child of a node
160
+ #
161
+ # @return [String]
162
+ #
163
+ # @api private
164
+ def text
165
+ first = @node
166
+ .children
167
+ .to_a
168
+ .filter(&:text?)
169
+ .first
170
+
171
+ first&.text
172
+ end
173
+ end
174
+ end
175
+ end
176
+ end