decanter 2.1.2 → 3.1.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. data/.codeclimate.yml +38 -0
  3. data/.github/CODEOWNERS +2 -0
  4. data/.github/ISSUE_TEMPLATE/BUG_REPORT.md +33 -0
  5. data/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md +18 -0
  6. data/.github/PULL_REQUEST_TEMPLATE.md +8 -0
  7. data/.gitignore +5 -0
  8. data/.rspec +2 -0
  9. data/.ruby-version +1 -0
  10. data/.travis.yml +10 -0
  11. data/CODE_OF_CONDUCT.md +76 -0
  12. data/CONTRIBUTING.md +36 -0
  13. data/Gemfile +4 -0
  14. data/Gemfile.lock +102 -0
  15. data/LICENSE.txt +21 -0
  16. data/README.md +241 -0
  17. data/Rakefile +1 -0
  18. data/bin/console +3 -4
  19. data/decanter.gemspec +39 -0
  20. data/lib/decanter.rb +18 -10
  21. data/lib/decanter/base.rb +0 -2
  22. data/lib/decanter/configuration.rb +2 -3
  23. data/lib/decanter/core.rb +136 -126
  24. data/lib/decanter/exceptions.rb +4 -7
  25. data/lib/decanter/extensions.rb +11 -11
  26. data/lib/decanter/parser.rb +13 -5
  27. data/lib/decanter/parser/array_parser.rb +28 -0
  28. data/lib/decanter/parser/base.rb +1 -2
  29. data/lib/decanter/parser/boolean_parser.rb +4 -3
  30. data/lib/decanter/parser/compose_parser.rb +27 -0
  31. data/lib/decanter/parser/core.rb +8 -16
  32. data/lib/decanter/parser/date_parser.rb +6 -7
  33. data/lib/decanter/parser/datetime_parser.rb +15 -0
  34. data/lib/decanter/parser/float_parser.rb +7 -5
  35. data/lib/decanter/parser/hash_parser.rb +6 -9
  36. data/lib/decanter/parser/integer_parser.rb +8 -4
  37. data/lib/decanter/parser/pass_parser.rb +5 -3
  38. data/lib/decanter/parser/phone_parser.rb +3 -3
  39. data/lib/decanter/parser/string_parser.rb +4 -5
  40. data/lib/decanter/parser/utils.rb +1 -3
  41. data/lib/decanter/parser/value_parser.rb +3 -4
  42. data/lib/decanter/railtie.rb +15 -11
  43. data/lib/decanter/version.rb +1 -3
  44. data/lib/generators/decanter/install_generator.rb +3 -5
  45. data/lib/generators/decanter/templates/initializer.rb +1 -4
  46. data/lib/generators/rails/decanter_generator.rb +5 -7
  47. data/lib/generators/rails/parser_generator.rb +3 -5
  48. data/lib/generators/rails/resource_override.rb +0 -2
  49. data/migration-guides/v3.0.0.md +21 -0
  50. metadata +49 -23
  51. data/lib/decanter/decant.rb +0 -11
  52. data/lib/decanter/parser/date_time_parser.rb +0 -21
  53. data/lib/decanter/parser/join_parser.rb +0 -14
  54. data/lib/decanter/parser/key_value_splitter_parser.rb +0 -18
  55. 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,28 @@
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.compact
25
+ end
26
+ end
27
+ end
28
+ 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