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.
- checksums.yaml +4 -4
- data/.codeclimate.yml +38 -0
- data/.gitignore +5 -0
- data/.rspec +2 -0
- data/.ruby-version +1 -0
- data/.travis.yml +10 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +102 -0
- data/LICENSE.txt +21 -0
- data/README.md +232 -0
- data/Rakefile +1 -0
- data/bin/console +3 -4
- data/decanter.gemspec +39 -0
- data/lib/decanter.rb +18 -10
- data/lib/decanter/base.rb +0 -2
- data/lib/decanter/configuration.rb +2 -3
- data/lib/decanter/core.rb +136 -126
- data/lib/decanter/exceptions.rb +4 -7
- data/lib/decanter/extensions.rb +11 -11
- data/lib/decanter/parser.rb +13 -5
- data/lib/decanter/parser/array_parser.rb +29 -0
- data/lib/decanter/parser/base.rb +1 -2
- data/lib/decanter/parser/boolean_parser.rb +4 -3
- data/lib/decanter/parser/compose_parser.rb +27 -0
- data/lib/decanter/parser/core.rb +8 -16
- data/lib/decanter/parser/date_parser.rb +6 -7
- data/lib/decanter/parser/datetime_parser.rb +15 -0
- data/lib/decanter/parser/float_parser.rb +7 -5
- data/lib/decanter/parser/hash_parser.rb +6 -9
- data/lib/decanter/parser/integer_parser.rb +8 -4
- data/lib/decanter/parser/pass_parser.rb +5 -3
- data/lib/decanter/parser/phone_parser.rb +3 -3
- data/lib/decanter/parser/string_parser.rb +4 -5
- data/lib/decanter/parser/utils.rb +1 -3
- data/lib/decanter/parser/value_parser.rb +3 -4
- data/lib/decanter/railtie.rb +15 -11
- data/lib/decanter/version.rb +1 -3
- data/lib/generators/decanter/install_generator.rb +3 -5
- data/lib/generators/decanter/templates/initializer.rb +1 -4
- data/lib/generators/rails/decanter_generator.rb +5 -7
- data/lib/generators/rails/parser_generator.rb +3 -5
- data/lib/generators/rails/resource_override.rb +0 -2
- data/migration-guides/v3.0.0.md +21 -0
- metadata +43 -23
- data/lib/decanter/decant.rb +0 -11
- data/lib/decanter/parser/date_time_parser.rb +0 -21
- data/lib/decanter/parser/join_parser.rb +0 -14
- data/lib/decanter/parser/key_value_splitter_parser.rb +0 -18
- data/lib/decanter/parser/time_parser.rb +0 -19
data/lib/decanter/exceptions.rb
CHANGED
@@ -1,9 +1,6 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
1
|
module Decanter
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
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
|
data/lib/decanter/extensions.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
34
|
+
self.new(decant(args, options))
|
35
|
+
.save!(context: options[:context])
|
36
36
|
end
|
37
37
|
|
38
|
-
def decant(args, options
|
39
|
-
if
|
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)
|
data/lib/decanter/parser.rb
CHANGED
@@ -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
|
27
|
-
end
|
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
|
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
|
data/lib/decanter/parser/base.rb
CHANGED
@@ -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,
|
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
|
+
|
data/lib/decanter/parser/core.rb
CHANGED
@@ -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(
|
13
|
-
if
|
14
|
-
|
15
|
-
elsif allowed?(values)
|
16
|
-
values
|
12
|
+
def parse(name, value, options={})
|
13
|
+
if allowed?(value)
|
14
|
+
{ name => value }
|
17
15
|
else
|
18
|
-
_parse(
|
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?(
|
44
|
-
@allowed &&
|
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
|
10
|
-
|
11
|
-
|
12
|
-
|
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
|
-
|
4
|
+
REGEX = /(\d|[.])/
|
5
|
+
|
6
|
+
allow Float, Integer
|
7
7
|
|
8
|
-
parser do |val,
|
9
|
-
|
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(
|
9
|
-
validate_hash(@parser.call(
|
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
|
-
|
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,
|
9
|
-
|
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,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,
|
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
|
-
|
7
|
-
|
8
|
-
|
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::'
|
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(
|
9
|
-
@parser.call(
|
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
|
+
|
data/lib/decanter/railtie.rb
CHANGED
@@ -1,17 +1,21 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
1
|
require 'decanter'
|
4
2
|
|
5
|
-
|
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
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
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
|