bumblebee 2.1.0 → 3.0.0

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