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