shale 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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