shale 0.8.0 → 0.9.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: 76c0022b3d2d42be143362c84c6697628781acdce7694fb9bc0f305d3f854964
4
- data.tar.gz: 0b0b1b060eab81ebfae80afa3c31c809280b66ebd77e413c5908c4bbd09a081a
3
+ metadata.gz: 3f880bc8d984c45e7e8a05bd155c1014df237e037263c7bf68a7ae82c0e49ea4
4
+ data.tar.gz: ba7a2f12bed87e048dd53d43900f1c8203d90f5f89a43b3824475ddda39eb07b
5
5
  SHA512:
6
- metadata.gz: 43bc9188d1f07c5c367a8ed94e2a3da6b878a7b7b86773d7898697abbd3937d4de6ae0471f9362c72e8a6be2cc5e5100a5206b2f2cc7155ff2ff8784953ef702
7
- data.tar.gz: 0e610703cc73809a78007785962d9a322e515b88ab143a55adfeaa1a0da5961f14d8768a3dec4ad797070bbab165c238123702cbc671f522f86e2934968ff78b
6
+ metadata.gz: 54023d4f621dae53537f73e0ad79d3736d5d52e605c1a7da2c4bdb06588b348feaae9066ce00b6b3aedc9e84ea23a425884eabf987717ffda6d3c22eccc6325f
7
+ data.tar.gz: e82cad292dab557891cad4f210bfb4a97d2096a6d2151a5a8aef020358accb5986e527e32a141a9a2e3a01d594e9a3adb42c0f7e8d17464517731136f63289a2
data/CHANGELOG.md CHANGED
@@ -1,3 +1,10 @@
1
+ ## [0.9.0] - 2022-10-31
2
+
3
+ ### Added
4
+ - Support for CSV
5
+ - Allow to specify version and add encoding to XML declaration
6
+ - Support for mapping/generating collections
7
+
1
8
  ## [0.8.0] - 2022-08-30
2
9
 
3
10
  ### 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, 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.
3
+ Shale is a Ruby object mapper and serializer for JSON, YAML, TOML, CSV and XML.
4
+ It allows you to parse JSON, YAML, TOML, CSV and XML data and convert it into Ruby data structures,
5
+ as well as serialize data structures into JSON, YAML, TOML, CSV 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, TOML and XML to Ruby data model
12
- * Convert Ruby data model to JSON, YAML, TOML and XML
11
+ * Convert JSON, YAML, TOML, CSV and XML to Ruby data model
12
+ * Convert Ruby data model to JSON, YAML, TOML, CSV 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, Tomlib, toml-rb, Nokogiri, REXML and Ox parsers
15
+ * Out of the box support for JSON, YAML, Tomlib, toml-rb, CSV, Nokogiri, REXML and Ox parsers
16
16
  * Support for custom adapters
17
17
 
18
18
  ## Installation
@@ -51,9 +51,13 @@ $ gem install shale
51
51
  * [Converting object to Hash](#converting-object-to-hash)
52
52
  * [Converting XML to object](#converting-xml-to-object)
53
53
  * [Converting object to XML](#converting-object-to-xml)
54
+ * [Converting CSV to object](#converting-csv-to-object)
55
+ * [Converting object to CSV](#converting-object-to-csv)
56
+ * [Converting collections](#converting-collections)
54
57
  * [Mapping JSON keys to object attributes](#mapping-json-keys-to-object-attributes)
55
58
  * [Mapping YAML keys to object attributes](#mapping-yaml-keys-to-object-attributes)
56
59
  * [Mapping TOML keys to object attributes](#mapping-toml-keys-to-object-attributes)
60
+ * [Mapping CSV columns to object attributes](#mapping-csv-columns-to-object-attributes)
57
61
  * [Mapping Hash keys to object attributes](#mapping-hash-keys-to-object-attributes)
58
62
  * [Mapping XML elements and attributes to object attributes](#mapping-xml-elements-and-attributes-to-object-attributes)
59
63
  * [Using XML namespaces](#using-xml-namespaces)
@@ -346,6 +350,70 @@ person.to_xml
346
350
  # </person>
347
351
  ```
348
352
 
353
+ ### Converting CSV to object
354
+
355
+ CSV represents a flat data structure, so you can't map properties to complex types directly,
356
+ but you can use methods to map properties to complex types
357
+ (see [Using methods to extract and generate data](#using-methods-to-extract-and-generate-data)
358
+ section).
359
+
360
+ `.from_csv` method allways returns an array of records.
361
+
362
+ ```ruby
363
+ people = Person.from_csv(<<~DATA)
364
+ John,Doe,50,false
365
+ DATA
366
+ ```
367
+
368
+ ### Converting object to CSV
369
+
370
+ ```ruby
371
+ people[0].to_csv # or Person.to_csv(people) if you want to convert a collection
372
+
373
+ # =>
374
+ #
375
+ # John,Doe,50,false
376
+ ```
377
+
378
+ ### Converting collections
379
+
380
+ Shale allows converting collections for formats that support it (JSON, YAML and CSV).
381
+ To convert Ruby array to JSON:
382
+
383
+ ```ruby
384
+ person1 = Person.new(name: 'John Doe')
385
+ person2 = Person.new(name: 'Joe Sixpack')
386
+
387
+ Person.to_json([person1, person2], pretty: true)
388
+ # or Person.to_yaml([person1, person2])
389
+ # or Person.to_csv([person1, person2])
390
+
391
+ # =>
392
+ #
393
+ # [
394
+ # { "name": "John Doe" },
395
+ # { "name": "Joe Sixpack" }
396
+ # ]
397
+ ```
398
+
399
+ To convert JSON array to Ruby:
400
+
401
+ ```ruby
402
+ Person.from_json(<<~JSON)
403
+ [
404
+ { "name": "John Doe" },
405
+ { "name": "Joe Sixpack" }
406
+ ]
407
+ JSON
408
+
409
+ # =>
410
+ #
411
+ # [
412
+ # #<Person:0x00000001033dbce8 @name="John Doe">,
413
+ # #<Person:0x00000001033db4c8 @name="Joe Sixpack">
414
+ # ]
415
+ ```
416
+
349
417
  ### Mapping JSON keys to object attributes
350
418
 
351
419
  By default keys are named the same as attributes. To use custom keys use:
@@ -392,6 +460,24 @@ class Person < Shale::Mapper
392
460
  end
393
461
  ```
394
462
 
463
+ ### Mapping CSV columns to object attributes
464
+
465
+ For CSV the order of mapping matters, the first argument in the `map` method is only
466
+ used as a label in header row. So, in the example below the first column will be mapped
467
+ to `:first_name` attribute and the second column to `:last_name`.
468
+
469
+ ```ruby
470
+ class Person < Shale::Mapper
471
+ attribute :first_name, Shale::Type::String
472
+ attribute :last_name, Shale::Type::String
473
+
474
+ csv do
475
+ map 'firstName', to: :first_name
476
+ map 'lastName', to: :last_name
477
+ end
478
+ end
479
+ ```
480
+
395
481
  ### Mapping Hash keys to object attributes
396
482
 
397
483
  ```ruby
@@ -561,8 +647,9 @@ DATA
561
647
 
562
648
  ### Rendering nil values
563
649
 
564
- By default elements with `nil` value are not rendered. You can change this behavior
565
- by using `render_nil: true` on a mapping.
650
+ For JSON, YAML, TOML and XML by default, elements with `nil` value are not rendered.
651
+ You can change this behavior by using `render_nil: true` on a mapping.
652
+ For CSV the default is to render `nil` elements.
566
653
 
567
654
  ```ruby
568
655
  class Person < Shale::Mapper
@@ -853,19 +940,52 @@ person.to_json(pretty: true)
853
940
  # }
854
941
  ```
855
942
 
856
- You can also add an XML declaration by passing `declaration: true` to `#to_xml`
943
+ You can also add an XML declaration by passing `declaration: true` and `encoding: true`
944
+ or if you want to spcify version: `declaration: '1.1'` and `encoding: 'ASCII'` to `#to_xml`
857
945
 
858
946
  ```ruby
859
- person.to_xml(pretty: true, declaration: true)
947
+ person.to_xml(pretty: true, declaration: true, encoding: true)
860
948
 
861
949
  # =>
862
950
  #
863
- # <?xml version="1.0"?>
951
+ # <?xml version="1.0" encoding="UTF-8"?>
864
952
  # <Person>
865
953
  # <Address city="London"/>
866
954
  # </Person>
867
955
  ```
868
956
 
957
+ For CSV you can pass `headers: true` to indicate that the first row contains column
958
+ names and shouldn't be included in the returned collection. It also accepts all the options that
959
+ [CSV parser](https://ruby-doc.org/stdlib-3.1.2/libdoc/csv/rdoc/CSV.html#class-CSV-label-Options) accepts.
960
+
961
+ ```ruby
962
+ class Person
963
+ attribute :first_name, Shale::Type::String
964
+ attribute :last_name, Shale::Type::String
965
+ end
966
+
967
+ people = Person.from_csv(<<~DATA, headers: true, col_sep: '|')
968
+ first_name|last_name
969
+ John|Doe
970
+ James|Sixpack
971
+ DATA
972
+
973
+ # =>
974
+ #
975
+ # [
976
+ # #<Person:0x0000000113d7a488 @first_name="John", @last_name="Doe">,
977
+ # #<Person:0x0000000113d7a488 @first_name="James", @last_name="Sixpack">
978
+ # ]
979
+
980
+ Person.to_csv(people, headers: true, col_sep: '|')
981
+
982
+ # =>
983
+ #
984
+ # first_name|last_name
985
+ # John|Doe
986
+ # James|Sixpack
987
+ ```
988
+
869
989
  ### Using custom models
870
990
 
871
991
  By default Shale combines mapper and model into one class. If you want to use your own classes
@@ -955,10 +1075,10 @@ end
955
1075
  ### Adapters
956
1076
 
957
1077
  Shale uses adapters for parsing and generating documents.
958
- By default Ruby's standard JSON, YAML parsers are used for handling JSON and YAML documents.
1078
+ By default Ruby's standard JSON, YAML, CSV parsers are used for handling JSON YAML, CSV documents.
959
1079
 
960
- You can change it by providing your own adapter. For JSON, YAML and TOML, adapter must implement
961
- `.load` and `.dump` class methods.
1080
+ You can change it by providing your own adapter. For JSON, YAML, TOML and CSV adapter must
1081
+ implement `.load` and `.dump` class methods.
962
1082
 
963
1083
  ```ruby
964
1084
  require 'shale'
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'csv'
4
+
5
+ module Shale
6
+ module Adapter
7
+ # CSV adapter
8
+ #
9
+ # @api public
10
+ class CSV
11
+ # Parse CSV into Array<Hash<String, any>>
12
+ #
13
+ # @param [String] csv CSV document
14
+ # @param [Array<String>] headers
15
+ # @param [Hash] options
16
+ #
17
+ # @return [Array<Hash<String, any>>]
18
+ #
19
+ # @api private
20
+ def self.load(csv, headers:, **options)
21
+ ::CSV.parse(csv, headers: headers, **options).map(&:to_h)
22
+ end
23
+
24
+ # Serialize Array<Hash<String, any>> into CSV
25
+ #
26
+ # @param [Array<Hash<String, any>>] obj Array<Hash<String, any>> object
27
+ # @param [Array<String>] headers
28
+ # @param [Hash] options
29
+ #
30
+ # @return [String]
31
+ #
32
+ # @api private
33
+ def self.dump(obj, headers:, **options)
34
+ ::CSV.generate(**options) do |csv|
35
+ obj.each do |row|
36
+ values = []
37
+
38
+ headers.each do |header|
39
+ values << row[header] if row.key?(header)
40
+ end
41
+
42
+ csv << values
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -9,9 +9,14 @@ module Shale
9
9
  class Document
10
10
  # Initialize object
11
11
  #
12
+ # @param [String, nil] version
13
+ #
12
14
  # @api private
13
- def initialize
14
- @doc = ::Nokogiri::XML::Document.new
15
+ def initialize(version = nil)
16
+ ver = nil
17
+ ver = version if version.is_a?(String)
18
+
19
+ @doc = ::Nokogiri::XML::Document.new(ver)
15
20
  @namespaces = {}
16
21
  end
17
22
 
@@ -37,12 +37,13 @@ module Shale
37
37
  #
38
38
  # @param [::Nokogiri::XML::Document] doc Nokogiri document
39
39
  # @param [true, false] pretty
40
- # @param [true, false] declaration
40
+ # @param [true, false, String] declaration
41
+ # @param [true, false, String] encoding
41
42
  #
42
43
  # @return [String]
43
44
  #
44
45
  # @api private
45
- def self.dump(doc, pretty: false, declaration: false)
46
+ def self.dump(doc, pretty: false, declaration: false, encoding: false)
46
47
  save_with = ::Nokogiri::XML::Node::SaveOptions::AS_XML
47
48
 
48
49
  if pretty
@@ -53,6 +54,10 @@ module Shale
53
54
  save_with |= ::Nokogiri::XML::Node::SaveOptions::NO_DECLARATION
54
55
  end
55
56
 
57
+ if encoding
58
+ doc.encoding = encoding == true ? 'UTF-8' : encoding
59
+ end
60
+
56
61
  result = doc.to_xml(save_with: save_with)
57
62
 
58
63
  unless pretty
@@ -64,9 +69,11 @@ module Shale
64
69
 
65
70
  # Create Shale::Adapter::Nokogiri::Document instance
66
71
  #
72
+ # @param [true, false, String, nil] declaration
73
+ #
67
74
  # @api private
68
- def self.create_document
69
- Document.new
75
+ def self.create_document(version = nil)
76
+ Document.new(version)
70
77
  end
71
78
  end
72
79
  end
@@ -31,12 +31,13 @@ module Shale
31
31
  #
32
32
  # @param [::Ox::Document, ::Ox::Element] doc Ox document
33
33
  # @param [true, false] pretty
34
- # @param [true, false] declaration
34
+ # @param [true, false, String] declaration
35
+ # @param [true, false, String] encoding
35
36
  #
36
37
  # @return [String]
37
38
  #
38
39
  # @api private
39
- def self.dump(doc, pretty: false, declaration: false)
40
+ def self.dump(doc, pretty: false, declaration: false, encoding: false)
40
41
  opts = { indent: -1, with_xml: false }
41
42
 
42
43
  if pretty
@@ -44,7 +45,12 @@ module Shale
44
45
  end
45
46
 
46
47
  if declaration
47
- doc[:version] = '1.0'
48
+ doc[:version] = declaration == true ? '1.0' : declaration
49
+
50
+ if encoding
51
+ doc[:encoding] = encoding == true ? 'UTF-8' : encoding
52
+ end
53
+
48
54
  opts[:with_xml] = true
49
55
  end
50
56
 
@@ -54,7 +60,7 @@ module Shale
54
60
  # Create Shale::Adapter::Ox::Document instance
55
61
  #
56
62
  # @api private
57
- def self.create_document
63
+ def self.create_document(_version = nil)
58
64
  Document.new
59
65
  end
60
66
  end
@@ -32,14 +32,28 @@ module Shale
32
32
  #
33
33
  # @param [::REXML::Document] doc REXML document
34
34
  # @param [true, false] pretty
35
- # @param [true, false] declaration
35
+ # @param [true, false, String] declaration
36
+ # @param [true, false, String] encoding
36
37
  #
37
38
  # @return [String]
38
39
  #
39
40
  # @api private
40
- def self.dump(doc, pretty: false, declaration: false)
41
+ def self.dump(doc, pretty: false, declaration: false, encoding: false)
41
42
  if declaration
42
- doc.add(::REXML::XMLDecl.new)
43
+ ver = nil
44
+ enc = nil
45
+
46
+ if declaration.is_a?(String)
47
+ ver = declaration
48
+ end
49
+
50
+ if encoding == true
51
+ enc = 'UTF-8'
52
+ else
53
+ enc = encoding || nil
54
+ end
55
+
56
+ doc.add(::REXML::XMLDecl.new(ver, enc))
43
57
  end
44
58
 
45
59
  io = StringIO.new
@@ -58,7 +72,7 @@ module Shale
58
72
  # Create Shale::Adapter::REXML::Document instance
59
73
  #
60
74
  # @api private
61
- def self.create_document
75
+ def self.create_document(_version = nil)
62
76
  Document.new
63
77
  end
64
78
  end
data/lib/shale/mapper.rb CHANGED
@@ -49,6 +49,7 @@ module Shale
49
49
  @json_mapping = Mapping::Dict.new
50
50
  @yaml_mapping = Mapping::Dict.new
51
51
  @toml_mapping = Mapping::Dict.new
52
+ @csv_mapping = Mapping::Dict.new(render_nil_default: true)
52
53
  @xml_mapping = Mapping::Xml.new
53
54
 
54
55
  class << self
@@ -87,6 +88,13 @@ module Shale
87
88
  # @api public
88
89
  attr_reader :toml_mapping
89
90
 
91
+ # Return CSV mapping object
92
+ #
93
+ # @return [Shale::Mapping::Dict]
94
+ #
95
+ # @api public
96
+ attr_reader :csv_mapping
97
+
90
98
  # Return XML mapping object
91
99
  #
92
100
  # @return [Shale::Mapping::XML]
@@ -109,12 +117,14 @@ module Shale
109
117
  subclass.instance_variable_set('@__json_mapping_init', @json_mapping.dup)
110
118
  subclass.instance_variable_set('@__yaml_mapping_init', @yaml_mapping.dup)
111
119
  subclass.instance_variable_set('@__toml_mapping_init', @toml_mapping.dup)
120
+ subclass.instance_variable_set('@__csv_mapping_init', @csv_mapping.dup)
112
121
  subclass.instance_variable_set('@__xml_mapping_init', @xml_mapping.dup)
113
122
 
114
123
  subclass.instance_variable_set('@hash_mapping', @hash_mapping.dup)
115
124
  subclass.instance_variable_set('@json_mapping', @json_mapping.dup)
116
125
  subclass.instance_variable_set('@yaml_mapping', @yaml_mapping.dup)
117
126
  subclass.instance_variable_set('@toml_mapping', @toml_mapping.dup)
127
+ subclass.instance_variable_set('@csv_mapping', @csv_mapping.dup)
118
128
 
119
129
  xml_mapping = @xml_mapping.dup
120
130
  xml_mapping.root(Utils.underscore(subclass.name || ''))
@@ -173,6 +183,7 @@ module Shale
173
183
  @json_mapping.map(name.to_s, to: name) unless @json_mapping.finalized?
174
184
  @yaml_mapping.map(name.to_s, to: name) unless @yaml_mapping.finalized?
175
185
  @toml_mapping.map(name.to_s, to: name) unless @toml_mapping.finalized?
186
+ @csv_mapping.map(name.to_s, to: name) unless @csv_mapping.finalized?
176
187
  @xml_mapping.map_element(name.to_s, to: name) unless @xml_mapping.finalized?
177
188
 
178
189
  @attributes_module.class_eval(<<-RUBY, __FILE__, __LINE__ + 1)
@@ -280,6 +291,30 @@ module Shale
280
291
  @toml_mapping.instance_eval(&block)
281
292
  end
282
293
 
294
+ # Define CSV mapping
295
+ #
296
+ # @param [Proc] block
297
+ #
298
+ # @example
299
+ # calss Person < Shale::Mapper
300
+ # attribute :first_name, Shale::Type::String
301
+ # attribute :last_name, Shale::Type::String
302
+ # attribute :age, Shale::Type::Integer
303
+ #
304
+ # csv do
305
+ # map 'first_name', to: :first_name
306
+ # map 'last_name', to: :last_name
307
+ # map 'age', to: :age
308
+ # end
309
+ # end
310
+ #
311
+ # @api public
312
+ def csv(&block)
313
+ @csv_mapping = @__csv_mapping_init.dup
314
+ @csv_mapping.finalize!
315
+ @csv_mapping.instance_eval(&block)
316
+ end
317
+
283
318
  # Define XML mapping
284
319
  #
285
320
  # @param [Proc] block
@@ -14,12 +14,12 @@ module Shale
14
14
  # @param [String] key Document's key
15
15
  # @param [Symbol, nil] to Object's attribute
16
16
  # @param [Hash, nil] using
17
- # @param [true, false] render_nil
17
+ # @param [true, false, nil] render_nil
18
18
  #
19
19
  # @raise [IncorrectMappingArgumentsError] when arguments are incorrect
20
20
  #
21
21
  # @api private
22
- def map(key, to: nil, using: nil, render_nil: false)
22
+ def map(key, to: nil, using: nil, render_nil: nil)
23
23
  super(key, to: to, using: using, render_nil: render_nil)
24
24
  end
25
25
 
@@ -18,10 +18,13 @@ module Shale
18
18
 
19
19
  # Initialize instance
20
20
  #
21
+ # @param [true, false] render_nil_default
22
+ #
21
23
  # @api private
22
- def initialize
24
+ def initialize(render_nil_default: false)
23
25
  @keys = {}
24
26
  @finalized = false
27
+ @render_nil_default = render_nil_default
25
28
  end
26
29
 
27
30
  # Map key to attribute
@@ -30,19 +33,19 @@ module Shale
30
33
  # @param [Symbol, nil] to
31
34
  # @param [Hash, nil] using
32
35
  # @param [String, nil] group
33
- # @param [true, false] render_nil
36
+ # @param [true, false, nil] render_nil
34
37
  #
35
38
  # @raise [IncorrectMappingArgumentsError] when arguments are incorrect
36
39
  #
37
40
  # @api private
38
- def map(key, to: nil, using: nil, group: nil, render_nil: false)
41
+ def map(key, to: nil, using: nil, group: nil, render_nil: nil)
39
42
  Validator.validate_arguments(key, to, using)
40
43
  @keys[key] = Descriptor::Dict.new(
41
44
  name: key,
42
45
  attribute: to,
43
46
  methods: using,
44
47
  group: group,
45
- render_nil: render_nil
48
+ render_nil: render_nil.nil? ? @render_nil_default : render_nil
46
49
  )
47
50
  end
48
51
 
@@ -13,11 +13,11 @@ module Shale
13
13
  # @api private
14
14
  class Complex < Value
15
15
  class << self
16
- %i[hash json yaml toml].each do |format|
16
+ %i[hash json yaml toml csv].each do |format|
17
17
  class_eval(<<-RUBY, __FILE__, __LINE__ + 1)
18
18
  # Convert Hash to Object using Hash/JSON/YAML/TOML mapping
19
19
  #
20
- # @param [Hash] hash Hash to convert
20
+ # @param [Hash, Array] hash Hash to convert
21
21
  # @param [Array<Symbol>] only
22
22
  # @param [Array<Symbol>] except
23
23
  # @param [any] context
@@ -26,6 +26,18 @@ module Shale
26
26
  #
27
27
  # @api public
28
28
  def of_#{format}(hash, only: nil, except: nil, context: nil)
29
+ #{
30
+ if format != :toml
31
+ <<~CODE
32
+ if hash.is_a?(Array)
33
+ return hash.map do |item|
34
+ of_#{format}(item, only: only, except: except, context: context)
35
+ end
36
+ end
37
+ CODE
38
+ end
39
+ }
40
+
29
41
  instance = model.new
30
42
 
31
43
  attributes
@@ -109,7 +121,7 @@ module Shale
109
121
 
110
122
  # Convert Object to Hash using Hash/JSON/YAML/TOML mapping
111
123
  #
112
- # @param [any] instance Object to convert
124
+ # @param [any, Array<any>] instance Object to convert
113
125
  # @param [Array<Symbol>] only
114
126
  # @param [Array<Symbol>] except
115
127
  # @param [any] context
@@ -120,6 +132,18 @@ module Shale
120
132
  #
121
133
  # @api public
122
134
  def as_#{format}(instance, only: nil, except: nil, context: nil)
135
+ #{
136
+ if format != :toml
137
+ <<~CODE
138
+ if instance.is_a?(Array)
139
+ return instance.map do |item|
140
+ as_#{format}(item, only: only, except: except, context: context)
141
+ end
142
+ end
143
+ CODE
144
+ end
145
+ }
146
+
123
147
  unless instance.is_a?(model)
124
148
  msg = "argument is a '\#{instance.class}' but should be a '\#{model}'"
125
149
  raise IncorrectModelError, msg
@@ -312,6 +336,65 @@ module Shale
312
336
  )
313
337
  end
314
338
 
339
+ # Convert CSV to Object
340
+ #
341
+ # @param [String] csv CSV to convert
342
+ # @param [Array<Symbol>] only
343
+ # @param [Array<Symbol>] except
344
+ # @param [any] context
345
+ # @param [true, false] headers
346
+ # @param [Hash] csv_options
347
+ #
348
+ # @return [model instance]
349
+ #
350
+ # @api public
351
+ def from_csv(csv, only: nil, except: nil, context: nil, headers: false, **csv_options)
352
+ data = Shale.csv_adapter.load(csv, **csv_options.merge(headers: csv_mapping.keys.keys))
353
+
354
+ data.shift if headers
355
+
356
+ of_csv(
357
+ data,
358
+ only: only,
359
+ except: except,
360
+ context: context
361
+ )
362
+ end
363
+
364
+ # Convert Object to CSV
365
+ #
366
+ # @param [model instance] instance Object to convert
367
+ # @param [Array<Symbol>] only
368
+ # @param [Array<Symbol>] except
369
+ # @param [any] context
370
+ # @param [true, false] headers
371
+ # @param [Hash] csv_options
372
+ #
373
+ # @return [String]
374
+ #
375
+ # @api public
376
+ def to_csv(instance, only: nil, except: nil, context: nil, headers: false, **csv_options)
377
+ data = as_csv([*instance], only: only, except: except, context: context)
378
+
379
+ cols = csv_mapping.keys.values
380
+
381
+ if only
382
+ cols = cols.select { |e| only.include?(e.attribute) }
383
+ end
384
+
385
+ if except
386
+ cols = cols.reject { |e| except.include?(e.attribute) }
387
+ end
388
+
389
+ cols = cols.map(&:name)
390
+
391
+ if headers
392
+ data.prepend(cols.to_h { |e| [e, e] })
393
+ end
394
+
395
+ Shale.csv_adapter.dump(data, **csv_options.merge(headers: cols))
396
+ end
397
+
315
398
  # Convert XML document to Object
316
399
  #
317
400
  # @param [Shale::Adapter::<XML adapter>::Node] element
@@ -495,7 +578,8 @@ module Shale
495
578
  _cdata = nil,
496
579
  only: nil,
497
580
  except: nil,
498
- context: nil
581
+ context: nil,
582
+ version: nil
499
583
  )
500
584
  unless instance.is_a?(model)
501
585
  msg = "argument is a '#{instance.class}' but should be a '#{model}'"
@@ -503,7 +587,7 @@ module Shale
503
587
  end
504
588
 
505
589
  unless doc
506
- doc = Shale.xml_adapter.create_document
590
+ doc = Shale.xml_adapter.create_document(version)
507
591
 
508
592
  element = as_xml(
509
593
  instance,
@@ -680,6 +764,7 @@ module Shale
680
764
  # @param [any] context
681
765
  # @param [true, false] pretty
682
766
  # @param [true, false] declaration
767
+ # @param [true, false, String] encoding
683
768
  #
684
769
  # @raise [AdapterError]
685
770
  #
@@ -692,13 +777,15 @@ module Shale
692
777
  except: nil,
693
778
  context: nil,
694
779
  pretty: false,
695
- declaration: false
780
+ declaration: false,
781
+ encoding: false
696
782
  )
697
783
  validate_xml_adapter
698
784
  Shale.xml_adapter.dump(
699
- as_xml(instance, only: only, except: except, context: context),
785
+ as_xml(instance, only: only, except: except, context: context, version: declaration),
700
786
  pretty: pretty,
701
- declaration: declaration
787
+ declaration: declaration,
788
+ encoding: encoding
702
789
  )
703
790
  end
704
791
 
@@ -801,6 +888,26 @@ module Shale
801
888
  self.class.to_toml(self, only: only, except: except, context: context)
802
889
  end
803
890
 
891
+ # Convert Object to CSV
892
+ #
893
+ # @param [Array<Symbol>] only
894
+ # @param [Array<Symbol>] except
895
+ # @param [any] context
896
+ #
897
+ # @return [String]
898
+ #
899
+ # @api public
900
+ def to_csv(only: nil, except: nil, context: nil, headers: false, **csv_options)
901
+ self.class.to_csv(
902
+ self,
903
+ only: only,
904
+ except: except,
905
+ context: context,
906
+ headers: headers,
907
+ **csv_options
908
+ )
909
+ end
910
+
804
911
  # Convert Object to XML
805
912
  #
806
913
  # @param [Array<Symbol>] only
@@ -808,18 +915,27 @@ module Shale
808
915
  # @param [any] context
809
916
  # @param [true, false] pretty
810
917
  # @param [true, false] declaration
918
+ # @param [true, false, String] encoding
811
919
  #
812
920
  # @return [String]
813
921
  #
814
922
  # @api public
815
- def to_xml(only: nil, except: nil, context: nil, pretty: false, declaration: false)
923
+ def to_xml(
924
+ only: nil,
925
+ except: nil,
926
+ context: nil,
927
+ pretty: false,
928
+ declaration: false,
929
+ encoding: false
930
+ )
816
931
  self.class.to_xml(
817
932
  self,
818
933
  only: only,
819
934
  except: except,
820
935
  context: context,
821
936
  pretty: pretty,
822
- declaration: declaration
937
+ declaration: declaration,
938
+ encoding: encoding
823
939
  )
824
940
  end
825
941
  end
@@ -47,6 +47,17 @@ module Shale
47
47
  value&.iso8601
48
48
  end
49
49
 
50
+ # Use ISO 8601 format in CSV document
51
+ #
52
+ # @param [Date] value
53
+ #
54
+ # @return [String]
55
+ #
56
+ # @api private
57
+ def self.as_csv(value, **)
58
+ value&.iso8601
59
+ end
60
+
50
61
  # Use ISO 8601 format in XML document
51
62
  #
52
63
  # @param [Date] value Value to convert to XML
@@ -47,6 +47,17 @@ module Shale
47
47
  value&.iso8601
48
48
  end
49
49
 
50
+ # Use ISO 8601 format in CSV document
51
+ #
52
+ # @param [Time] value
53
+ #
54
+ # @return [String]
55
+ #
56
+ # @api private
57
+ def self.as_csv(value, **)
58
+ value&.iso8601
59
+ end
60
+
50
61
  # Use ISO 8601 format in XML document
51
62
  #
52
63
  # @param [Time] value Value to convert to XML
@@ -111,6 +111,28 @@ module Shale
111
111
  value
112
112
  end
113
113
 
114
+ # Extract value from CSV document
115
+ #
116
+ # @param [any] value
117
+ #
118
+ # @return [any]
119
+ #
120
+ # @api private
121
+ def of_csv(value, **)
122
+ value
123
+ end
124
+
125
+ # Convert value to form accepted by CSV document
126
+ #
127
+ # @param [any] value
128
+ #
129
+ # @return [any]
130
+ #
131
+ # @api private
132
+ def as_csv(value, **)
133
+ value
134
+ end
135
+
114
136
  # Extract value from XML document
115
137
  #
116
138
  # @param [Shale::Adapter::<XML adapter>::Node] value
data/lib/shale/version.rb CHANGED
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Shale
4
4
  # @api private
5
- VERSION = '0.8.0'
5
+ VERSION = '0.9.0'
6
6
  end
data/lib/shale.rb CHANGED
@@ -3,6 +3,7 @@
3
3
  require 'yaml'
4
4
 
5
5
  require_relative 'shale/mapper'
6
+ require_relative 'shale/adapter/csv'
6
7
  require_relative 'shale/adapter/json'
7
8
  require_relative 'shale/type/boolean'
8
9
  require_relative 'shale/type/date'
@@ -15,7 +16,7 @@ require_relative 'shale/version'
15
16
  # Main library namespace
16
17
  #
17
18
  # Shale uses adapters for parsing and serializing documents.
18
- # For handling JSON and YAML, adapter must implement .load and .dump methods, so
19
+ # For handling JSON, YAML, TOML and CSV, adapter must implement .load and .dump methods, so
19
20
  # e.g for handling JSON, MultiJson works out of the box.
20
21
  #
21
22
  # Adapters for XML handling are more complicated and must conform to [@see shale/adapter/rexml]
@@ -29,9 +30,11 @@ require_relative 'shale/version'
29
30
  # Shale.json_adapter = MultiJson
30
31
  # Shale.json_adapter # => MultiJson
31
32
  #
32
- # @example setting YAML adapter for handling YAML documents
33
- # Shale.yaml_adapter = MultiJson
34
- # Shale.yaml_adapter # => MultiJson
33
+ # @example setting TOML adapter for handling TOML documents
34
+ # require 'shale/adapter/toml_rb'
35
+ #
36
+ # Shale.toml_adapter = Shale::Adapter::TomlRB
37
+ # Shale.toml_adapter # => Shale::Adapter::TomlRB
35
38
  #
36
39
  # @example setting REXML adapter for handling XML documents
37
40
  # Shale.xml_adapter = Shale::Adapter::REXML
@@ -86,6 +89,16 @@ module Shale
86
89
  # @api public
87
90
  attr_accessor :toml_adapter
88
91
 
92
+ # Set CSV adapter
93
+ #
94
+ # @param [.load, .dump] adapter
95
+ #
96
+ # @example
97
+ # Shale.csv_adapter = Shale::Adapter::CSV
98
+ #
99
+ # @api public
100
+ attr_writer :csv_adapter
101
+
89
102
  # XML adapter accessor. Available adapters are Shale::Adapter::REXML,
90
103
  # Shale::Adapter::Nokogiri and Shale::Adapter::Ox
91
104
  #
@@ -126,5 +139,18 @@ module Shale
126
139
  def yaml_adapter
127
140
  @yaml_adapter || YAML
128
141
  end
142
+
143
+ # Return CSV adapter. By default CSV is used
144
+ #
145
+ # @return [.load, .dump]
146
+ #
147
+ # @example
148
+ # Shale.csv_adapter
149
+ # # => Shale::Adapter::CSV
150
+ #
151
+ # @api public
152
+ def csv_adapter
153
+ @csv_adapter || Adapter::CSV
154
+ end
129
155
  end
130
156
  end
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, TOML and YAML.'
12
- spec.description = 'Ruby object mapper and serializer for XML, JSON, TOML and YAML.'
11
+ spec.summary = 'Ruby object mapper and serializer for XML, JSON, TOML, CSV and YAML.'
12
+ spec.description = 'Ruby object mapper and serializer for XML, JSON, TOML, CSV 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.8.0
4
+ version: 0.9.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-08-30 00:00:00.000000000 Z
11
+ date: 2022-10-31 00:00:00.000000000 Z
12
12
  dependencies: []
13
- description: Ruby object mapper and serializer for XML, JSON, TOML and YAML.
13
+ description: Ruby object mapper and serializer for XML, JSON, TOML, CSV and YAML.
14
14
  email:
15
15
  - beerkg@gmail.com
16
16
  executables:
@@ -23,6 +23,7 @@ files:
23
23
  - README.md
24
24
  - exe/shaleb
25
25
  - lib/shale.rb
26
+ - lib/shale/adapter/csv.rb
26
27
  - lib/shale/adapter/json.rb
27
28
  - lib/shale/adapter/nokogiri.rb
28
29
  - lib/shale/adapter/nokogiri/document.rb
@@ -126,5 +127,5 @@ requirements: []
126
127
  rubygems_version: 3.3.7
127
128
  signing_key:
128
129
  specification_version: 4
129
- summary: Ruby object mapper and serializer for XML, JSON, TOML and YAML.
130
+ summary: Ruby object mapper and serializer for XML, JSON, TOML, CSV and YAML.
130
131
  test_files: []