decanter 2.1.2 → 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.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/.codeclimate.yml +38 -0
  3. data/.gitignore +5 -0
  4. data/.rspec +2 -0
  5. data/.ruby-version +1 -0
  6. data/.travis.yml +10 -0
  7. data/Gemfile +4 -0
  8. data/Gemfile.lock +102 -0
  9. data/LICENSE.txt +21 -0
  10. data/README.md +232 -0
  11. data/Rakefile +1 -0
  12. data/bin/console +3 -4
  13. data/decanter.gemspec +39 -0
  14. data/lib/decanter.rb +18 -10
  15. data/lib/decanter/base.rb +0 -2
  16. data/lib/decanter/configuration.rb +2 -3
  17. data/lib/decanter/core.rb +136 -126
  18. data/lib/decanter/exceptions.rb +4 -7
  19. data/lib/decanter/extensions.rb +11 -11
  20. data/lib/decanter/parser.rb +13 -5
  21. data/lib/decanter/parser/array_parser.rb +29 -0
  22. data/lib/decanter/parser/base.rb +1 -2
  23. data/lib/decanter/parser/boolean_parser.rb +4 -3
  24. data/lib/decanter/parser/compose_parser.rb +27 -0
  25. data/lib/decanter/parser/core.rb +8 -16
  26. data/lib/decanter/parser/date_parser.rb +6 -7
  27. data/lib/decanter/parser/datetime_parser.rb +15 -0
  28. data/lib/decanter/parser/float_parser.rb +7 -5
  29. data/lib/decanter/parser/hash_parser.rb +6 -9
  30. data/lib/decanter/parser/integer_parser.rb +8 -4
  31. data/lib/decanter/parser/pass_parser.rb +5 -3
  32. data/lib/decanter/parser/phone_parser.rb +3 -3
  33. data/lib/decanter/parser/string_parser.rb +4 -5
  34. data/lib/decanter/parser/utils.rb +1 -3
  35. data/lib/decanter/parser/value_parser.rb +3 -4
  36. data/lib/decanter/railtie.rb +15 -11
  37. data/lib/decanter/version.rb +1 -3
  38. data/lib/generators/decanter/install_generator.rb +3 -5
  39. data/lib/generators/decanter/templates/initializer.rb +1 -4
  40. data/lib/generators/rails/decanter_generator.rb +5 -7
  41. data/lib/generators/rails/parser_generator.rb +3 -5
  42. data/lib/generators/rails/resource_override.rb +0 -2
  43. data/migration-guides/v3.0.0.md +21 -0
  44. metadata +43 -23
  45. data/lib/decanter/decant.rb +0 -11
  46. data/lib/decanter/parser/date_time_parser.rb +0 -21
  47. data/lib/decanter/parser/join_parser.rb +0 -14
  48. data/lib/decanter/parser/key_value_splitter_parser.rb +0 -18
  49. data/lib/decanter/parser/time_parser.rb +0 -19
@@ -1,9 +1,6 @@
1
- # frozen_string_literal: true
2
-
3
1
  module Decanter
4
- module Core
5
- class Error < StandardError; end
6
- class UnhandledKeysError < Error; end
7
- class MissingRequiredInputValue < Error; end
8
- end
2
+ class Error < StandardError; end
3
+ class UnhandledKeysError < Error; end
4
+ class MissingRequiredInputValue < Error; end
5
+ class ParseError < Error; end
9
6
  end
@@ -1,19 +1,18 @@
1
- # frozen_string_literal: true
2
-
3
1
  module Decanter
4
2
  module Extensions
3
+
5
4
  def self.included(base)
6
5
  base.extend(ClassMethods)
7
6
  end
8
7
 
9
8
  def decant_update(args, **options)
10
9
  self.attributes = self.class.decant(args, options)
11
- save(context: options[:context])
10
+ self.save(context: options[:context])
12
11
  end
13
12
 
14
13
  def decant_update!(args, **options)
15
14
  self.attributes = self.class.decant(args, options)
16
- save!(context: options[:context])
15
+ self.save!(context: options[:context])
17
16
  end
18
17
 
19
18
  def decant(args, **options)
@@ -21,22 +20,23 @@ module Decanter
21
20
  end
22
21
 
23
22
  module ClassMethods
23
+
24
24
  def decant_create(args, **options)
25
- new(decant(args, options))
26
- .save(context: options[:context])
25
+ self.new(decant(args, options))
26
+ .save(context: options[:context])
27
27
  end
28
28
 
29
29
  def decant_new(args, **options)
30
- new(decant(args, options))
30
+ self.new(decant(args, options))
31
31
  end
32
32
 
33
33
  def decant_create!(args, **options)
34
- new(decant(args, options))
35
- .save!(context: options[:context])
34
+ self.new(decant(args, options))
35
+ .save!(context: options[:context])
36
36
  end
37
37
 
38
- def decant(args, options = {})
39
- if (specified_decanter = options[:decanter])
38
+ def decant(args, options={})
39
+ if specified_decanter = options[:decanter]
40
40
  Decanter.decanter_from(specified_decanter)
41
41
  else
42
42
  Decanter.decanter_for(self)
@@ -1,5 +1,3 @@
1
- # frozen_string_literal: true
2
-
3
1
  require_relative 'parser/utils'
4
2
 
5
3
  module Decanter
@@ -15,6 +13,16 @@ module Decanter
15
13
  .flatten
16
14
  end
17
15
 
16
+ # Composes multiple parsers into a single parser
17
+ def compose_parsers(parsers)
18
+ raise ArgumentError.new('expects an array') unless parsers.is_a? Array
19
+ composed_parser = Class.new(Decanter::Parser::ComposeParser)
20
+ composed_parser.parsers(parsers)
21
+ composed_parser
22
+ end
23
+
24
+ private
25
+
18
26
  # convert from a class or symbol to a string and concat 'Parser'
19
27
  def klass_or_sym_to_str(klass_or_sym)
20
28
  case klass_or_sym
@@ -23,8 +31,8 @@ module Decanter
23
31
  when Symbol
24
32
  symbol_to_string(klass_or_sym)
25
33
  else
26
- raise ArgumentError, "cannot lookup parser for #{klass_or_sym} with class #{klass_or_sym.class}"
27
- end + 'Parser'
34
+ raise ArgumentError.new("cannot lookup parser for #{klass_or_sym} with class #{klass_or_sym.class}")
35
+ end.concat('Parser')
28
36
  end
29
37
 
30
38
  # convert from a string to a constant
@@ -32,7 +40,7 @@ module Decanter
32
40
  # safe_constantize returns nil if match not found
33
41
  parser_str.safe_constantize ||
34
42
  concat_str(parser_str).safe_constantize ||
35
- raise(NameError, "cannot find parser #{parser_str}")
43
+ raise(NameError.new("cannot find parser #{parser_str}"))
36
44
  end
37
45
 
38
46
  # expand to include preparsers
@@ -0,0 +1,29 @@
1
+ module Decanter
2
+ module Parser
3
+ class ArrayParser < ValueParser
4
+
5
+ DUMMY_VALUE_KEY = '_'.freeze
6
+
7
+ parser do |val, options|
8
+ next if val.nil?
9
+ raise Decanter::ParseError.new 'Expects an array' unless val.is_a? Array
10
+ # Fetch parser classes for provided keys
11
+ parse_each = options.fetch(:parse_each, :pass)
12
+ item_parsers = Parser.parsers_for(Array.wrap(parse_each))
13
+ unless item_parsers.all? { |parser| parser <= ValueParser }
14
+ raise Decanter::ParseError.new 'parser(s) for array items must subclass ValueParser'
15
+ end
16
+ # Compose supplied parsers
17
+ item_parser = Parser.compose_parsers(item_parsers)
18
+ # Parse all values
19
+ val.map do |item|
20
+ # Value parsers will expect a "key" for the value they're parsing,
21
+ # so we provide a dummy one.
22
+ result = item_parser.parse(DUMMY_VALUE_KEY, item, options)
23
+ result[DUMMY_VALUE_KEY]
24
+ end
25
+ end
26
+
27
+ end
28
+ end
29
+ end
@@ -1,5 +1,3 @@
1
- # frozen_string_literal: true
2
-
3
1
  require_relative 'core'
4
2
 
5
3
  module Decanter
@@ -9,3 +7,4 @@ module Decanter
9
7
  end
10
8
  end
11
9
  end
10
+
@@ -1,11 +1,12 @@
1
- # frozen_string_literal: true
2
-
3
1
  module Decanter
4
2
  module Parser
5
3
  class BooleanParser < ValueParser
4
+
6
5
  allow TrueClass, FalseClass
7
6
 
8
- parser do |val, _options|
7
+ parser do |val, options|
8
+ raise Decanter::ParseError.new 'Expects a single value' if val.is_a? Array
9
+ next if (val.nil? || val === '')
9
10
  [1, '1'].include?(val) || !!/true/i.match(val.to_s)
10
11
  end
11
12
  end
@@ -0,0 +1,27 @@
1
+ require_relative 'core'
2
+
3
+ # A parser that composes the results of multiple parsers.
4
+ # Intended for internal use only.
5
+ module Decanter
6
+ module Parser
7
+ class ComposeParser < Base
8
+
9
+ def self._parse(name, value, options={})
10
+ raise Decanter::ParseError.new('Must have parsers') unless @parsers
11
+ # Call each parser on the result of the previous one.
12
+ initial_result = { name => value }
13
+ @parsers.reduce(initial_result) do |result, parser|
14
+ result.keys.reduce({}) do |acc, key|
15
+ acc.merge(parser.parse(key, result[key], options))
16
+ end
17
+ end
18
+ end
19
+
20
+ def self.parsers(parsers)
21
+ @parsers = parsers
22
+ end
23
+
24
+ end
25
+ end
26
+ end
27
+
@@ -1,21 +1,19 @@
1
- # frozen_string_literal: true
2
-
3
1
  module Decanter
4
2
  module Parser
5
3
  module Core
4
+
6
5
  def self.included(base)
7
6
  base.extend(ClassMethods)
8
7
  end
9
8
 
10
9
  module ClassMethods
10
+
11
11
  # Check if allowed, parse if not
12
- def parse(values, options = {})
13
- if empty_values?(values)
14
- nil
15
- elsif allowed?(values)
16
- values
12
+ def parse(name, value, options={})
13
+ if allowed?(value)
14
+ { name => value }
17
15
  else
18
- _parse(values, options)
16
+ _parse(name, value, options)
19
17
  end
20
18
  end
21
19
 
@@ -40,14 +38,8 @@ module Decanter
40
38
  end
41
39
 
42
40
  # Check for allowed classes
43
- def allowed?(values)
44
- @allowed && Array.wrap(values).all? do |value|
45
- @allowed.any? { |allowed| value.is_a? allowed }
46
- end
47
- end
48
-
49
- def empty_values?(values)
50
- return true if Array.wrap(values).all? { |value| value.nil? || value == '' }
41
+ def allowed?(value)
42
+ @allowed && @allowed.any? { |allowed| value.is_a? allowed }
51
43
  end
52
44
  end
53
45
  end
@@ -1,17 +1,16 @@
1
- # frozen_string_literal: true
2
-
3
1
  module Decanter
4
2
  module Parser
5
3
  class DateParser < ValueParser
4
+
6
5
  allow Date
7
6
 
8
7
  parser do |val, options|
9
- if (parse_format = options[:parse_format])
10
- ::Date.strptime(val, parse_format)
11
- else
12
- ::Date.parse(val)
13
- end
8
+ raise Decanter::ParseError.new 'Expects a single value' if val.is_a? Array
9
+ next if (val.nil? || val === '')
10
+ parse_format = options.fetch(:parse_format, '%m/%d/%Y')
11
+ ::Date.strptime(val, parse_format)
14
12
  end
15
13
  end
16
14
  end
17
15
  end
16
+
@@ -0,0 +1,15 @@
1
+ module Decanter
2
+ module Parser
3
+ class DateTimeParser < ValueParser
4
+
5
+ allow DateTime
6
+
7
+ parser do |val, options|
8
+ raise Decanter::ParseError.new 'Expects a single value' if val.is_a? Array
9
+ next if (val.nil? || val === '')
10
+ parse_format = options.fetch(:parse_format, '%m/%d/%Y %I:%M:%S %p')
11
+ ::DateTime.strptime(val, parse_format)
12
+ end
13
+ end
14
+ end
15
+ end
@@ -1,12 +1,14 @@
1
- # frozen_string_literal: true
2
-
3
1
  module Decanter
4
2
  module Parser
5
3
  class FloatParser < ValueParser
6
- allow Float
4
+ REGEX = /(\d|[.])/
5
+
6
+ allow Float, Integer
7
7
 
8
- parser do |val, _options|
9
- Float(val)
8
+ parser do |val, options|
9
+ raise Decanter::ParseError.new 'Expects a single value' if val.is_a? Array
10
+ next if (val.nil? || val === '')
11
+ val.scan(REGEX).join.try(:to_f)
10
12
  end
11
13
  end
12
14
  end
@@ -1,21 +1,18 @@
1
- # frozen_string_literal: true
2
-
3
1
  require_relative 'core'
4
2
 
5
3
  module Decanter
6
4
  module Parser
7
5
  class HashParser < Base
8
- def self._parse(values, options = {})
9
- validate_hash(@parser.call(values, options))
6
+ def self._parse(name, value, options={})
7
+ validate_hash(@parser.call(name, value, options))
10
8
  end
11
9
 
10
+ private
12
11
  def self.validate_hash(parsed)
13
- return parsed if parsed.is_a?(Hash)
14
-
15
- raise(ArgumentError, "Result of HashParser was #{parsed} when it must be a hash.")
12
+ parsed.is_a?(Hash) ? parsed :
13
+ raise(ArgumentError.new("Result of HashParser #{self.name} was #{parsed} when it must be a hash."))
16
14
  end
17
-
18
- private_class_method :validate_hash
19
15
  end
20
16
  end
21
17
  end
18
+
@@ -1,12 +1,16 @@
1
- # frozen_string_literal: true
2
-
3
1
  module Decanter
4
2
  module Parser
5
3
  class IntegerParser < ValueParser
4
+ REGEX = /(\d|[.])/
5
+
6
6
  allow Integer
7
7
 
8
- parser do |val, _options|
9
- Integer(val)
8
+ parser do |val, options|
9
+ raise Decanter::ParseError.new 'Expects a single value' if val.is_a? Array
10
+ next if (val.nil? || val === '')
11
+ val.is_a?(Float) ?
12
+ val.to_i :
13
+ val.scan(REGEX).join.try(:to_i)
10
14
  end
11
15
  end
12
16
  end
@@ -1,9 +1,11 @@
1
- # frozen_string_literal: true
2
-
3
1
  module Decanter
4
2
  module Parser
5
3
  class PassParser < ValueParser
6
- allow Object
4
+
5
+ parser do |val, options|
6
+ next if (val.nil? || val == '')
7
+ val
8
+ end
7
9
  end
8
10
  end
9
11
  end
@@ -1,5 +1,3 @@
1
- # frozen_string_literal: true
2
-
3
1
  module Decanter
4
2
  module Parser
5
3
  class PhoneParser < ValueParser
@@ -7,7 +5,9 @@ module Decanter
7
5
 
8
6
  allow Integer
9
7
 
10
- parser do |val, _options|
8
+ parser do |val, options|
9
+ raise Decanter::ParseError.new 'Expects a single value' if val.is_a? Array
10
+ next if (val.nil? || val === '')
11
11
  val.scan(REGEX).join.to_s
12
12
  end
13
13
  end
@@ -1,11 +1,10 @@
1
- # frozen_string_literal: true
2
-
3
1
  module Decanter
4
2
  module Parser
5
3
  class StringParser < ValueParser
6
- allow String
7
-
8
- parser do |val, _options|
4
+ parser do |val, options|
5
+ raise Decanter::ParseError.new 'Expects a single value' if val.is_a? Array
6
+ next if (val.nil? || val === '')
7
+ next val if val.is_a? String
9
8
  val.to_s
10
9
  end
11
10
  end
@@ -1,5 +1,3 @@
1
- # frozen_string_literal: true
2
-
3
1
  module Decanter
4
2
  module Parser
5
3
  module Utils
@@ -26,7 +24,7 @@ module Decanter
26
24
  end
27
25
 
28
26
  def concat_str(parser_str)
29
- 'Decanter::Parser::' + parser_str
27
+ 'Decanter::Parser::'.concat(parser_str)
30
28
  end
31
29
  end
32
30
  end
@@ -1,13 +1,12 @@
1
- # frozen_string_literal: true
2
-
3
1
  require_relative 'core'
4
2
 
5
3
  module Decanter
6
4
  module Parser
7
5
  class ValueParser < Base
8
- def self._parse(values, options = {})
9
- @parser.call(values, options)
6
+ def self._parse(name, value, options={})
7
+ { name => @parser.call(value, options) }
10
8
  end
11
9
  end
12
10
  end
13
11
  end
12
+
@@ -1,17 +1,21 @@
1
- # frozen_string_literal: true
2
-
3
1
  require 'decanter'
4
2
 
5
- module Decanter
6
- class Railtie < Rails::Railtie
7
- initializer 'decanter.parser.autoload', before: :set_autoload_paths do |app|
8
- app.config.autoload_paths << Rails.root.join('lib/decanter/parsers')
9
- end
3
+ class Decanter::Railtie < Rails::Railtie
10
4
 
11
- generators do |app|
12
- Rails::Generators.configure!(app.config.generators)
13
- Rails::Generators.hidden_namespaces.uniq!
14
- require 'generators/rails/resource_override'
5
+ initializer 'decanter.active_record' do
6
+ ActiveSupport.on_load :active_record do
7
+ require 'decanter/extensions'
8
+ Decanter::Extensions::ActiveRecordExtensions.enable!
15
9
  end
16
10
  end
11
+
12
+ initializer 'decanter.parser.autoload', :before => :set_autoload_paths do |app|
13
+ app.config.autoload_paths << Rails.root.join("lib/decanter/parsers")
14
+ end
15
+
16
+ generators do |app|
17
+ Rails::Generators.configure!(app.config.generators)
18
+ Rails::Generators.hidden_namespaces.uniq!
19
+ require 'generators/rails/resource_override'
20
+ end
17
21
  end