bumblebee 2.1.0 → 3.0.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.
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # Copyright (c) 2018-present, Blue Marble Payroll, LLC
5
+ #
6
+ # This source code is licensed under the MIT license found in the
7
+ # LICENSE file in the root directory of this source tree.
8
+ #
9
+
10
+ module Bumblebee
11
+ # A Mutator is a composition of a converter with hash value setting. It can be a straight
12
+ # converter, or it can be new types which are not directly defined as 'converters.'
13
+ class Mutator
14
+ module Types
15
+ IGNORE = :ignore
16
+ end
17
+ include ::Bumblebee::Mutator::Types
18
+
19
+ attr_reader :converter, :type
20
+
21
+ def initialize(arg)
22
+ if arg.nil?
23
+ @type = nil
24
+ @converter = ::Bumblebee::NullConverter.new
25
+ elsif mutator?(arg)
26
+ @type = ::Bumblebee::Mutator::Types.const_get(arg.to_s.upcase.to_sym)
27
+ @converter = ::Bumblebee::NullConverter.new
28
+ else
29
+ @type = nil
30
+ @converter = ::Bumblebee::SimpleConverter.new(arg)
31
+ end
32
+
33
+ freeze
34
+ end
35
+
36
+ def set(object, key, val)
37
+ return object if ignore?
38
+
39
+ ::Bumblebee::ObjectInterface.set(object, key, converter.convert(val))
40
+ end
41
+
42
+ private
43
+
44
+ def ignore?
45
+ type == IGNORE
46
+ end
47
+
48
+ def mutator?(arg)
49
+ return false unless arg.is_a?(String) || arg.is_a?(Symbol)
50
+
51
+ ::Bumblebee::Mutator::Types.constants.include?(arg.to_s.upcase.to_sym)
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # Copyright (c) 2018-present, Blue Marble Payroll, LLC
5
+ #
6
+ # This source code is licensed under the MIT license found in the
7
+ # LICENSE file in the root directory of this source tree.
8
+ #
9
+
10
+ module Bumblebee
11
+ # Base converter using the Null Object Pattern. Use this when a custom converter is not needed.
12
+ class NullConverter
13
+ def convert(val)
14
+ val
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # Copyright (c) 2018-present, Blue Marble Payroll, LLC
5
+ #
6
+ # This source code is licensed under the MIT license found in the
7
+ # LICENSE file in the root directory of this source tree.
8
+ #
9
+
10
+ module Bumblebee
11
+ # Provides methods for interacting with custom objects.
12
+ class ObjectInterface
13
+ class << self
14
+ def traverse(object, through)
15
+ pointer = object
16
+
17
+ through.each do |t|
18
+ next unless pointer
19
+
20
+ pointer = get(pointer, t)
21
+ end
22
+
23
+ pointer
24
+ end
25
+
26
+ def build(object, through)
27
+ pointer = object
28
+
29
+ through.each do |t|
30
+ pointer = get(pointer, t) || get(set(pointer, t, pointer.class.new), t)
31
+ end
32
+
33
+ pointer
34
+ end
35
+
36
+ def set(object, key, val)
37
+ object.tap do |o|
38
+ setter_method = "#{key}="
39
+ if o.respond_to?(setter_method)
40
+ o.send(setter_method, val)
41
+ elsif o.respond_to?(:[])
42
+ o[key] = val
43
+ end
44
+ end
45
+ end
46
+
47
+ def get(object, key)
48
+ if object.is_a?(Hash)
49
+ indifferent_hash_get(object, key)
50
+ elsif object.respond_to?(key)
51
+ object.send(key)
52
+ end
53
+ end
54
+
55
+ private
56
+
57
+ def indifferent_hash_get(hash, key)
58
+ if hash.key?(key.to_s)
59
+ hash[key.to_s]
60
+ elsif hash.key?(key.to_s.to_sym)
61
+ hash[key.to_s.to_sym]
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # Copyright (c) 2018-present, Blue Marble Payroll, LLC
5
+ #
6
+ # This source code is licensed under the MIT license found in the
7
+ # LICENSE file in the root directory of this source tree.
8
+ #
9
+
10
+ module Bumblebee
11
+ # Subclass of Converter that provides a simple implementation for each Type.
12
+ class SimpleConverter < ::Bumblebee::Converter
13
+ DEFAULT_DATE = '1900-01-01'
14
+ DEFAULT_BIG_DECIMAL = 0
15
+
16
+ private
17
+
18
+ def process_pluck_join(val)
19
+ raise ArgumentError, 'sub_property is required for a pluck_join' unless sub_property
20
+
21
+ Array(val).map { |h| per.convert(h.is_a?(Hash) ? h[sub_property] : nil) }
22
+ .join(separator)
23
+ end
24
+
25
+ def process_pluck_split(val)
26
+ raise ArgumentError, 'sub_property is required for a pluck_split' unless sub_property
27
+
28
+ process_split(val).map do |v|
29
+ object_class.new.tap { |h| ::Bumblebee::ObjectInterface.set(h, sub_property, v) }
30
+ end
31
+ end
32
+
33
+ def process_ignore(_val)
34
+ nil
35
+ end
36
+
37
+ def process_join(val)
38
+ Array(val).map { |v| per.convert(v) }.join(separator)
39
+ end
40
+
41
+ def process_split(val)
42
+ val.to_s.split(separator).map { |v| per.convert(v) }
43
+ end
44
+
45
+ def process_function(val)
46
+ raise ArgumentError, 'function is required for function type' unless function
47
+
48
+ function.call(val)
49
+ end
50
+
51
+ def process_date(val)
52
+ return nil if nullable? && null_or_empty?(val)
53
+
54
+ Date.strptime(null_or_empty_default(val, DEFAULT_DATE).to_s, date_format)
55
+ end
56
+
57
+ def process_string(val)
58
+ return nil if nullable? && null_or_empty?(val)
59
+
60
+ val.to_s
61
+ end
62
+
63
+ def process_integer(val)
64
+ return nil if nullable? && null_or_empty?(val)
65
+
66
+ val.to_i
67
+ end
68
+
69
+ def process_float(val)
70
+ return nil if nullable? && null_or_empty?(val)
71
+
72
+ val.to_f
73
+ end
74
+
75
+ def process_bigdecimal(val)
76
+ return nil if nullable? && null_or_empty?(val)
77
+
78
+ BigDecimal(null_or_empty_default(val, DEFAULT_BIG_DECIMAL).to_s)
79
+ end
80
+
81
+ def process_boolean(val)
82
+ if nullable? && nully?(val)
83
+ nil
84
+ elsif truthy?(val)
85
+ true
86
+ else
87
+ false
88
+ end
89
+ end
90
+
91
+ def null_or_empty_default(val, default)
92
+ null_or_empty?(val) ? default : val
93
+ end
94
+
95
+ def null_or_empty?(val)
96
+ val.nil? || val.to_s.empty?
97
+ end
98
+
99
+ # rubocop:disable Style/DoubleNegation
100
+ def nully?(val)
101
+ null_or_empty?(val) || !!(val.to_s =~ /(nil|null)$/i)
102
+ end
103
+
104
+ def truthy?(val)
105
+ !!(val.to_s =~ /(true|t|yes|y|1)$/i)
106
+ end
107
+ # rubocop:enable Style/DoubleNegation
108
+ end
109
+ end
@@ -10,30 +10,20 @@
10
10
  module Bumblebee
11
11
  # Wraps up columns and provides to main methods:
12
12
  # generate_csv: take in an array of objects and return a string (CSV contents)
13
- # parse_csv: take in a string and return an array of OpenStruct objects
13
+ # parse_csv: take in a string and return an array of hashes
14
14
  class Template
15
- class << self
16
- def columns
17
- @columns ||= []
18
- end
15
+ extend Forwardable
16
+ extend ::Bumblebee::ColumnDsl
19
17
 
20
- def column(field, opts = {})
21
- columns << ::Bumblebee::Column.make(opts.merge(field: field))
18
+ def_delegators :column_set, :headers, :columns
22
19
 
23
- self
24
- end
20
+ attr_reader :object_class
25
21
 
26
- def all_columns
27
- ancestors.reverse_each.inject([]) do |arr, ancestor|
28
- ancestor < ::Bumblebee::Template ? arr + ancestor.columns : arr
29
- end
30
- end
31
- end
32
-
33
- attr_reader :columns
22
+ def initialize(columns: nil, object_class: Hash, &block)
23
+ @column_set = ::Bumblebee::ColumnSet.new(self.class.all_columns)
24
+ @object_class = object_class
34
25
 
35
- def initialize(columns = [], &block)
36
- @columns = self.class.all_columns + ::Bumblebee::Column.array(columns)
26
+ column_set.add(columns)
37
27
 
38
28
  return unless block_given?
39
29
 
@@ -44,43 +34,39 @@ module Bumblebee
44
34
  end
45
35
  end
46
36
 
47
- # New DSL method to use for adding columns
48
- def column(field, opts = {})
49
- @columns << ::Bumblebee::Column.make(opts.merge(field: field))
50
- self
51
- end
37
+ def column(header, opts = {})
38
+ column_set.column(header, opts)
52
39
 
53
- # Return array of strings (headers)
54
- def headers
55
- columns.map(&:header)
40
+ self
56
41
  end
57
42
 
58
- def generate_csv(objects, options = {})
43
+ def generate(objects, options = {})
59
44
  objects = objects.is_a?(Hash) ? [objects] : Array(objects)
60
45
 
61
46
  write_options = options.merge(headers: headers, write_headers: true)
62
47
 
63
48
  CSV.generate(write_options) do |csv|
64
49
  objects.each do |object|
65
- row = columns.map { |column| column.object_to_csv(object) }
66
-
67
- csv << row
50
+ csv << columns.each_with_object({}) do |column, hash|
51
+ column.csv_set(object, hash)
52
+ end
68
53
  end
69
54
  end
70
55
  end
71
56
 
72
- def parse_csv(string, options = {})
57
+ def parse(string, options = {})
73
58
  csv = CSV.new(string, options.merge(headers: true))
74
59
 
75
- # Drop the first record, it is the header record
76
60
  csv.to_a.map do |row|
77
61
  # Build up a hash using the column one at a time
78
- extracted_hash = columns.inject({}) do |hash, column|
79
- hash.merge(column.csv_to_object(row))
62
+ columns.each_with_object(object_class.new) do |column, object|
63
+ column.object_set(row, object)
80
64
  end
81
-
82
- extracted_hash
83
65
  end
84
66
  end
67
+
68
+ private
69
+
70
+ attr_reader :column_set
85
71
  end
86
72
  end
@@ -8,5 +8,5 @@
8
8
  #
9
9
 
10
10
  module Bumblebee
11
- VERSION = '2.1.0'
11
+ VERSION = '3.0.0'
12
12
  end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # Copyright (c) 2018-present, Blue Marble Payroll, LLC
5
+ #
6
+ # This source code is licensed under the MIT license found in the
7
+ # LICENSE file in the root directory of this source tree.
8
+ #
9
+
10
+ require 'spec_helper'
11
+ require './spec/examples/converter_test_case'
12
+
13
+ describe ::Bumblebee::SimpleConverter do
14
+ describe '#convert' do
15
+ ConverterTestCase.all.each do |test_case|
16
+ it "should convert: #{test_case.arg}" do
17
+ converter = ::Bumblebee::SimpleConverter.new(test_case.arg)
18
+
19
+ test_case.convert_cases.each do |convert_case|
20
+ input = convert_case.first
21
+ output = convert_case.last
22
+
23
+ expect(converter.convert(input)).to eq(output)
24
+ expect(converter.convert(input).class).to eq(output.class)
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -8,65 +8,180 @@
8
8
  #
9
9
 
10
10
  require 'spec_helper'
11
+ require 'examples/person_template'
12
+ require 'examples/simple_object'
11
13
 
12
14
  describe ::Bumblebee::Template do
13
- let(:field) { :id }
14
- let(:opts) { { header: 'ID #' } }
15
+ describe 'array/string-based columns and symbol based object keys' do
16
+ let(:data_objects) { yaml_fixture('simple', 'data.yml').map(&:symbolize_keys) }
15
17
 
16
- context 'with a blank template' do
17
- let(:template) { ::Bumblebee::Template.new }
18
+ let(:csv_file) { fixture('simple', 'data.csv') }
18
19
 
19
- subject { template }
20
+ let(:columns) { yaml_fixture('simple', 'columns.yml') }
20
21
 
21
- it '#column should add a column' do
22
- subject.column(field, opts)
22
+ subject { ::Bumblebee::Template.new(columns: columns) }
23
23
 
24
- expect(subject.columns.length).to eq(1)
25
- expect(subject.columns.first.field).to eq(field)
26
- expect(subject.columns.first.header).to eq(opts[:header])
24
+ specify '#generate_csv properly builds a CSV-formatted string' do
25
+ actual = subject.generate(data_objects)
26
+
27
+ expect(actual).to eq(csv_file)
28
+ end
29
+
30
+ specify '#parse_csv properly builds objects' do
31
+ actual = subject.parse(csv_file)
32
+
33
+ actual = actual.map(&:symbolize_keys)
34
+
35
+ expect(actual).to eq(data_objects)
27
36
  end
28
37
  end
29
38
 
30
- describe '#initialize' do
31
- specify 'that initialization accepts a block (with arity) for column creation' do
32
- template = ::Bumblebee::Template.new do |t|
33
- t.column field, opts
34
- end
39
+ describe 'array/string-based columns and OpenStruct objects' do
40
+ let(:data_objects) do
41
+ yaml_fixture('simple', 'data.yml').map { |h| OpenStruct.new(h.symbolize_keys) }
42
+ end
43
+
44
+ let(:csv_file) { fixture('simple', 'data.csv') }
35
45
 
36
- expect(template.columns.length).to eq(1)
37
- expect(template.columns.first.field).to eq(field)
38
- expect(template.columns.first.header).to eq(opts[:header])
46
+ let(:columns) { yaml_fixture('simple', 'columns.yml') }
47
+
48
+ subject { ::Bumblebee::Template.new(columns: columns, object_class: OpenStruct) }
49
+
50
+ specify '#generate_csv properly builds a CSV-formatted string' do
51
+ actual = subject.generate(data_objects)
52
+
53
+ expect(actual).to eq(csv_file)
54
+ end
55
+
56
+ specify '#parse_csv properly builds objects' do
57
+ actual = subject.parse(csv_file)
58
+
59
+ expect(actual).to eq(data_objects)
39
60
  end
61
+ end
62
+
63
+ describe 'array/string-based columns and custom objects' do
64
+ let(:data_objects) do
65
+ yaml_fixture('simple', 'data.yml').map { |h| SimpleObject.new(h.symbolize_keys) }
66
+ end
67
+
68
+ let(:csv_file) { fixture('simple', 'data.csv') }
69
+
70
+ let(:columns) { yaml_fixture('simple', 'columns.yml') }
71
+
72
+ subject { ::Bumblebee::Template.new(columns: columns, object_class: SimpleObject) }
73
+
74
+ specify '#generate_csv properly builds a CSV-formatted string' do
75
+ actual = subject.generate(data_objects)
76
+
77
+ expect(actual).to eq(csv_file)
78
+ end
79
+
80
+ specify '#parse_csv properly builds objects' do
81
+ actual = subject.parse(csv_file)
82
+
83
+ expect(actual).to eq(data_objects)
84
+ end
85
+ end
86
+
87
+ describe 'config-based columns' do
88
+ let(:data_objects) { yaml_fixture('registrations', 'data.yml') }
89
+
90
+ let(:csv_file) { fixture('registrations', 'data.csv') }
91
+
92
+ let(:columns) { yaml_fixture('registrations', 'columns.yml') }
93
+
94
+ subject { ::Bumblebee::Template.new(columns: columns) }
40
95
 
41
- specify 'that initialization accepts a block (without arity) for column creation' do
42
- template = ::Bumblebee::Template.new do
43
- column :id, header: 'ID #'
96
+ specify '#generate_csv properly builds a CSV-formatted string' do
97
+ actual = subject.generate(data_objects)
98
+
99
+ expect(actual).to eq(csv_file)
100
+ end
101
+
102
+ specify '#parse_csv properly builds objects' do
103
+ actual = subject.parse(csv_file)
104
+
105
+ expect(actual).to eq(data_objects)
106
+ end
107
+ end
108
+
109
+ describe 'block-based (with local context) columns' do
110
+ let(:data_objects) { yaml_fixture('registrations', 'data.yml') }
111
+
112
+ let(:csv_file) { fixture('registrations', 'data.csv') }
113
+
114
+ let(:columns) { yaml_fixture('registrations', 'columns.yml') }
115
+
116
+ subject do
117
+ ::Bumblebee::Template.new do |t|
118
+ columns.each do |header, opts|
119
+ t.column(header, opts)
120
+ end
44
121
  end
122
+ end
123
+
124
+ specify '#generate_csv properly builds a CSV-formatted string' do
125
+ actual = subject.generate(data_objects)
126
+
127
+ expect(actual).to eq(csv_file)
128
+ end
129
+
130
+ specify '#parse_csv properly builds objects' do
131
+ actual = subject.parse(csv_file)
45
132
 
46
- expect(template.columns.length).to eq(1)
47
- expect(template.columns.first.field).to eq(field)
48
- expect(template.columns.first.header).to eq(opts[:header])
133
+ expect(actual).to eq(data_objects)
49
134
  end
50
135
  end
51
136
 
52
- describe 'subclassing' do
53
- class PersonTemplate < ::Bumblebee::Template
54
- column :id, header: 'ID #'
137
+ describe 'block-based (without local context) columns' do
138
+ let(:data_objects) { yaml_fixture('registrations', 'data.yml') }
139
+
140
+ let(:csv_file) { fixture('registrations', 'data.csv') }
141
+
142
+ subject do
143
+ ::Bumblebee::Template.new do
144
+ columns = yaml_fixture('registrations', 'columns.yml')
145
+ columns.each do |header, opts|
146
+ column(header, opts)
147
+ end
148
+ end
149
+ end
150
+
151
+ specify '#generate_csv properly builds a CSV-formatted string' do
152
+ actual = subject.generate(data_objects)
153
+
154
+ expect(actual).to eq(csv_file)
55
155
  end
56
156
 
57
- class CompletePersonTemplate < PersonTemplate
58
- column :first, header: 'First Name'
157
+ specify '#parse_csv properly builds objects' do
158
+ actual = subject.parse(csv_file)
159
+
160
+ expect(actual).to eq(data_objects)
161
+ end
162
+ end
163
+
164
+ describe 'class-based columns' do
165
+ let(:data_objects) { yaml_fixture('people', 'data.yml') }
166
+
167
+ let(:csv_file) { fixture('people', 'data.csv') }
168
+
169
+ subject { PersonTemplate.new }
170
+
171
+ specify '#generate_csv properly builds a CSV-formatted string' do
172
+ template = PersonTemplate.new
173
+
174
+ actual = template.generate(data_objects)
175
+
176
+ expect(actual).to eq(csv_file)
59
177
  end
60
178
 
61
- it 'should use class-level declared columns in the correct parenting hierarchical order' do
62
- template = CompletePersonTemplate.new
179
+ specify '#parse_csv properly builds objects' do
180
+ template = PersonTemplate.new
63
181
 
64
- expect(template.columns.length).to eq(2)
65
- expect(template.columns.first.field).to eq(:id)
66
- expect(template.columns.first.header).to eq('ID #')
182
+ actual = template.parse(csv_file)
67
183
 
68
- expect(template.columns.last.field).to eq(:first)
69
- expect(template.columns.last.header).to eq('First Name')
184
+ expect(actual).to eq(data_objects)
70
185
  end
71
186
  end
72
187
  end