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.
@@ -7,9 +7,5 @@ require 'bumblebee'
7
7
  # You can add fixtures and/or initialization code here to make experimenting
8
8
  # with your gem easier. You can also use a different console, if you like.
9
9
 
10
- # (If you use this, don't forget to add pry to your Gemfile!)
11
- # require "pry"
12
- # Pry.start
13
-
14
- require 'irb'
15
- IRB.start
10
+ require 'pry'
11
+ Pry.start
@@ -7,68 +7,22 @@
7
7
  # LICENSE file in the root directory of this source tree.
8
8
  #
9
9
 
10
+ require 'bigdecimal'
10
11
  require 'csv'
11
- require 'acts_as_hashable'
12
- require 'ostruct'
13
-
12
+ require 'date'
13
+ require 'forwardable'
14
+
15
+ # Monkey-patching core libaries
16
+ require_relative 'core_ext/hash'
17
+ Hash.include ::Bumblebee::CoreExt::Hash
18
+
19
+ # Load library
20
+ require_relative 'object_interface'
21
+ require_relative 'mutator'
22
+ require_relative 'null_converter'
23
+ require_relative 'converter'
24
+ require_relative 'simple_converter'
14
25
  require_relative 'column'
26
+ require_relative 'column_set'
27
+ require_relative 'column_dsl'
15
28
  require_relative 'template'
16
-
17
- # The top-level module provides the two main methods for convenience.
18
- # You can also consume these in a more OOP way using the Template class or a more
19
- # procedural way using these.
20
- module Bumblebee
21
- class << self
22
- # Two signatures for consumption:
23
- #
24
- # ::Bumblebee.generate_csv(columns = [], objects = [], options = {})
25
- #
26
- # or
27
- #
28
- # ::Bumblebee.generate_csv(objects = [], options = {}) do |t|
29
- # t.column :id, header: 'ID #'
30
- # t.column :first, header: 'First Name'
31
- # end
32
- def generate_csv(*args, &block)
33
- if block_given?
34
- objects = args[0] || []
35
- options = args[1] || {}
36
- else
37
- objects = args[1] || []
38
- options = args[2] || {}
39
- end
40
-
41
- template(args, &block).generate_csv(objects, options)
42
- end
43
-
44
- # Two signatures for consumption:
45
- #
46
- # ::Bumblebee.parse_csv(columns = [], string = '', options = {})
47
- #
48
- # or
49
- #
50
- # ::Bumblebee.parse_csv(string = '', options = {}) do |t|
51
- # t.column :id, header: 'ID #'
52
- # t.column :first, header: 'First Name'
53
- # end
54
- def parse_csv(*args, &block)
55
- if block_given?
56
- string = args[0] || ''
57
- options = args[1] || {}
58
- else
59
- string = args[1] || ''
60
- options = args[2] || {}
61
- end
62
-
63
- template(args, &block).parse_csv(string, options)
64
- end
65
-
66
- private
67
-
68
- def template(args, &block)
69
- columns = block_given? ? [] : (args[0] || [])
70
-
71
- ::Bumblebee::Template.new(columns, &block)
72
- end
73
- end
74
- end
@@ -8,83 +8,61 @@
8
8
  #
9
9
 
10
10
  module Bumblebee
11
- # This is main piece of logic that defines a column which can go from objects to csv cell values
12
- # and csv rows to objects.
11
+ # This is main piece of logic that defines a column which can go
12
+ # from objects to csv cell values and csv rows to objects.
13
13
  class Column
14
- acts_as_hashable
15
-
16
- attr_reader :field,
17
- :to_object,
18
- :header,
19
- :to_csv
20
-
21
- def initialize(field:, header: nil, to_csv: nil, to_object: nil)
22
- raise ArgumentError, 'field is required' unless field
23
-
24
- @field = field
25
- @header = make_header(header || field)
26
- @to_csv = Array(to_csv || field)
27
- @to_object = Array(to_object || @header)
14
+ attr_reader :header,
15
+ :property,
16
+ :through,
17
+ :to_csv,
18
+ :to_object
19
+
20
+ def initialize(
21
+ header,
22
+ property: nil,
23
+ through: [],
24
+ to_csv: nil,
25
+ to_object: nil
26
+ )
27
+ raise ArgumentError, 'header is required' if header.to_s.empty?
28
+
29
+ @header = header.to_s
30
+ @property = property || @header
31
+ @through = Array(through)
32
+ @to_csv = ::Bumblebee::Mutator.new(to_csv)
33
+ @to_object = ::Bumblebee::Mutator.new(to_object)
34
+
35
+ freeze
28
36
  end
29
37
 
30
- # Take a object and convert to a value.
31
- def object_to_csv(object)
32
- val = object
33
-
34
- # Iterate over keys until we reach a nil or the end of keys.
35
- to_csv.each do |f|
36
- # short-circuit out of the extract method
37
- return nil unless val
38
+ # Extract from object and set on hash
39
+ def csv_set(data_object, hash)
40
+ value = extract(traverse(data_object), property)
38
41
 
39
- val = single_extract(val, f)
40
- end
41
-
42
- val
42
+ to_csv.set(hash, header, value)
43
43
  end
44
44
 
45
- def csv_to_object(csv_hash)
46
- return nil unless csv_hash
47
-
48
- value = csv_hash
45
+ def object_set(csv_object, hash)
46
+ pointer = build(hash)
47
+ value = csv_object[header]
49
48
 
50
- to_object.each do |f|
51
- value = single_extract(value, f)
52
- end
49
+ to_object.set(pointer, property, value)
53
50
 
54
- hash = {}
55
- hash.tap { hash[field] = value }
51
+ hash
56
52
  end
57
53
 
58
54
  private
59
55
 
60
- # Loop through all values, contcatenating their string equivalent with an underscore.
61
- # Since we allow procs, but we want deterministic headers, we will simply transform
62
- # theem to _proc_. This is not perfect and could create naming clashes, but it really
63
- # should not be. You should really leave proc's out of header and field.
64
- def make_header(value)
65
- Array(value).map do |v|
66
- if v.is_a?(Proc)
67
- 'proc'
68
- else
69
- v.to_s
70
- end
71
- end.join('_')
56
+ def traverse(object)
57
+ ::Bumblebee::ObjectInterface.traverse(object, through)
58
+ end
59
+
60
+ def extract(object, key)
61
+ ::Bumblebee::ObjectInterface.get(object, key)
72
62
  end
73
63
 
74
- # Take an object and attempt to extract a value from it.
75
- # First, see if the key is a proc, if so, then delegate to the proc.
76
- # Next, see if the object responds to the key, if so, then call it.
77
- # Lastly, see if it responds to brackets, if so, then call the brackets method sending
78
- # in the key.
79
- # Finally if all else fails, return nil.
80
- def single_extract(object, key)
81
- if key.is_a?(Proc)
82
- key.call(object)
83
- elsif object.respond_to?(key)
84
- object.send(key)
85
- elsif object.respond_to?(:[])
86
- object[key]
87
- end
64
+ def build(object)
65
+ ::Bumblebee::ObjectInterface.build(object, through)
88
66
  end
89
67
  end
90
68
  end
@@ -0,0 +1,38 @@
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
+ # Defines a class-level interace for specifying columns.
12
+ module ColumnDsl
13
+ extend Forwardable
14
+
15
+ def_delegators :column_set, :columns
16
+
17
+ def column_set
18
+ @column_set ||= ::Bumblebee::ColumnSet.new
19
+ end
20
+
21
+ def column(header, opts = {})
22
+ column_set.column(header, opts)
23
+
24
+ self
25
+ end
26
+
27
+ def all_column_sets
28
+ # the reverse preserves the order of inheritance to go from parent -> child
29
+ ancestors.reverse_each.with_object(::Bumblebee::ColumnSet.new) do |ancestor, set|
30
+ ancestor < ::Bumblebee::Template ? set.add(ancestor.columns) : set
31
+ end
32
+ end
33
+
34
+ def all_columns
35
+ all_column_sets.columns
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,83 @@
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
+ # Maintains a list (dictionary) of columns where each header can only exist once.
12
+ # If columns with same header are re-added, then they are overwritten.
13
+ # This class also provides a factory interface through #add for adding columns
14
+ class ColumnSet
15
+ extend Forwardable
16
+
17
+ FACTORY_ADD_METHODS = [
18
+ [::Bumblebee::Column, :assign],
19
+ [Hash, :add_header_column_hash]
20
+ ].freeze
21
+
22
+ private_constant :FACTORY_ADD_METHODS
23
+
24
+ def_delegator :column_hash, :keys, :headers
25
+
26
+ def_delegator :column_hash, :values, :columns
27
+
28
+ def initialize(columns = nil)
29
+ @column_hash = {}
30
+
31
+ add(columns)
32
+ end
33
+
34
+ def column(header, opts = {})
35
+ column = ::Bumblebee::Column.new(header, opts.symbolize_keys)
36
+
37
+ column_hash[column.header] = column
38
+
39
+ self
40
+ end
41
+
42
+ def add(*args)
43
+ args.flatten.compact.each { |arg| factory_add(arg) }
44
+
45
+ self
46
+ end
47
+
48
+ private
49
+
50
+ attr_reader :column_hash
51
+
52
+ def assign(column)
53
+ column_hash[column.header] = column
54
+
55
+ self
56
+ end
57
+
58
+ def add_header_column_hash(hash)
59
+ hash.each_pair do |header, opts|
60
+ column = ::Bumblebee::Column.new(header, opts.symbolize_keys)
61
+
62
+ assign(column)
63
+ end
64
+
65
+ self
66
+ end
67
+
68
+ def factory_add(arg)
69
+ FACTORY_ADD_METHODS.each do |factory_add_method|
70
+ class_constant = factory_add_method.first
71
+ method_sym = factory_add_method.last
72
+
73
+ if arg.is_a?(class_constant)
74
+ send(method_sym, arg)
75
+ return self
76
+ end
77
+ end
78
+
79
+ # Base case, use arg as the header
80
+ column(arg)
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,97 @@
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 class that defines the general interface and structure for a converter class.
12
+ # Subclasses should use the 'visitor' pattern and imeplement a visit_* method for each of the
13
+ # Types.
14
+ class Converter
15
+ module Types
16
+ BIGDECIMAL = :bigdecimal
17
+ BOOLEAN = :boolean
18
+ DATE = :date
19
+ INTEGER = :integer
20
+ JOIN = :join
21
+ FLOAT = :float
22
+ FUNCTION = :function
23
+ PLUCK_JOIN = :pluck_join
24
+ PLUCK_SPLIT = :pluck_split
25
+ SPLIT = :split
26
+ STRING = :string
27
+ end
28
+ include Types
29
+
30
+ DEFAULT_DATE_FORMAT = '%Y-%m-%d'
31
+ DEFAULT_SEPARATOR = ','
32
+ VISITOR_METHOD_PREFIX = 'process_'
33
+
34
+ private_constant :VISITOR_METHOD_PREFIX
35
+
36
+ attr_reader :function,
37
+ :object_class,
38
+ :per,
39
+ :sub_property,
40
+ :type
41
+
42
+ def initialize(arg)
43
+ if arg.is_a?(Proc)
44
+ @type = FUNCTION
45
+ @function = arg
46
+ @object_class = Hash
47
+ elsif arg.is_a?(Hash)
48
+ hash = arg.symbolize_keys
49
+ initialize_from_hash(hash)
50
+ else
51
+ @type = make_type(arg)
52
+ @per = make_converter
53
+ @object_class = Hash
54
+ end
55
+
56
+ freeze
57
+ end
58
+
59
+ def convert(val)
60
+ send("#{VISITOR_METHOD_PREFIX}#{type}", val)
61
+ end
62
+
63
+ private
64
+
65
+ def make_type(val)
66
+ Types.const_get(val.to_s.upcase.to_sym)
67
+ end
68
+
69
+ def initialize_from_hash(hash)
70
+ @type = make_type(hash[:type])
71
+ @function = hash[:function]
72
+ @nullable = hash[:nullable]
73
+ @separator = hash[:separator]
74
+ @date_format = hash[:date_format]
75
+ @sub_property = hash[:sub_property]
76
+ @object_class = hash[:object_class] || Hash
77
+ @per = make_converter(hash[:per])
78
+ end
79
+
80
+ def nullable
81
+ @nullable || false
82
+ end
83
+ alias nullable? nullable
84
+
85
+ def separator
86
+ @separator || DEFAULT_SEPARATOR
87
+ end
88
+
89
+ def date_format
90
+ @date_format || DEFAULT_DATE_FORMAT
91
+ end
92
+
93
+ def make_converter(arg = nil)
94
+ arg ? self.class.new(arg) : ::Bumblebee::NullConverter.new
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,21 @@
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
+ class CoreExt
12
+ # Monkey-patches for the core Hash class. These will be manually mixed in separately.
13
+ module Hash
14
+ unless method_defined?(:symbolize_keys)
15
+ def symbolize_keys
16
+ map { |k, v| [k.to_sym, v] }.to_h
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end