bumblebee 2.1.0 → 3.0.0

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