hcast 0.0.1
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.
- data/.gitignore +4 -0
- data/.travis.yml +3 -0
- data/Gemfile +8 -0
- data/Gemfile.lock +35 -0
- data/LICENSE.txt +22 -0
- data/README.md +113 -0
- data/Rakefile +1 -0
- data/hcast.gemspec +23 -0
- data/lib/hcast.rb +46 -0
- data/lib/hcast/attributes_caster.rb +83 -0
- data/lib/hcast/attributes_parser.rb +63 -0
- data/lib/hcast/caster.rb +142 -0
- data/lib/hcast/casters.rb +13 -0
- data/lib/hcast/casters/array_caster.rb +32 -0
- data/lib/hcast/casters/boolean_caster.rb +15 -0
- data/lib/hcast/casters/date_caster.rb +17 -0
- data/lib/hcast/casters/datetime_caster.rb +19 -0
- data/lib/hcast/casters/float_caster.rb +17 -0
- data/lib/hcast/casters/hash_caster.rb +11 -0
- data/lib/hcast/casters/integer_caster.rb +17 -0
- data/lib/hcast/casters/string_caster.rb +13 -0
- data/lib/hcast/casters/symbol_caster.rb +18 -0
- data/lib/hcast/casters/time_caster.rb +17 -0
- data/lib/hcast/concern.rb +136 -0
- data/lib/hcast/config.rb +11 -0
- data/lib/hcast/errors.rb +38 -0
- data/lib/hcast/metadata/attribute.rb +26 -0
- data/lib/hcast/version.rb +3 -0
- data/spec/hcast/caster_spec.rb +339 -0
- data/spec/hcast/hcast_spec.rb +37 -0
- data/spec/spec_helper.rb +8 -0
- metadata +113 -0
@@ -0,0 +1,13 @@
|
|
1
|
+
# List of build in casters
|
2
|
+
module HCast::Casters
|
3
|
+
require 'hcast/casters/array_caster'
|
4
|
+
require 'hcast/casters/boolean_caster'
|
5
|
+
require 'hcast/casters/date_caster'
|
6
|
+
require 'hcast/casters/datetime_caster'
|
7
|
+
require 'hcast/casters/float_caster'
|
8
|
+
require 'hcast/casters/hash_caster'
|
9
|
+
require 'hcast/casters/integer_caster'
|
10
|
+
require 'hcast/casters/string_caster'
|
11
|
+
require 'hcast/casters/symbol_caster'
|
12
|
+
require 'hcast/casters/time_caster'
|
13
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
class HCast::Casters::ArrayCaster
|
2
|
+
|
3
|
+
def self.cast(value, attr_name, options = {})
|
4
|
+
if value.is_a?(Array)
|
5
|
+
if options[:each]
|
6
|
+
cast_array_items(value, attr_name, options)
|
7
|
+
else
|
8
|
+
value
|
9
|
+
end
|
10
|
+
else
|
11
|
+
raise HCast::Errors::CastingError, "#{attr_name} should be an array"
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def self.cast_array_items(array, attr_name, options)
|
18
|
+
caster_name = options[:each]
|
19
|
+
caster = HCast.casters[caster_name]
|
20
|
+
check_caster_exists!(caster, caster_name)
|
21
|
+
array.map do |item|
|
22
|
+
caster.cast(item, "#{attr_name} item", options)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.check_caster_exists!(caster, caster_name)
|
27
|
+
unless caster
|
28
|
+
raise HCast::Errors::CasterNotFoundError, "caster with name #{caster_name} is not found"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
class HCast::Casters::BooleanCaster
|
2
|
+
|
3
|
+
def self.cast(value, attr_name, options = {})
|
4
|
+
if [TrueClass, FalseClass].include?(value.class)
|
5
|
+
value
|
6
|
+
elsif ['1', 'true', 'on', 1].include?(value)
|
7
|
+
true
|
8
|
+
elsif ['0', 'false', 'off', 0].include?(value)
|
9
|
+
false
|
10
|
+
else
|
11
|
+
raise HCast::Errors::CastingError, "#{attr_name} should be a boolean"
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
class HCast::Casters::DateCaster
|
2
|
+
|
3
|
+
def self.cast(value, attr_name, options = {})
|
4
|
+
if value.is_a?(Date)
|
5
|
+
value
|
6
|
+
elsif value.is_a?(String)
|
7
|
+
begin
|
8
|
+
Date.parse(value)
|
9
|
+
rescue ArgumentError => e
|
10
|
+
raise HCast::Errors::CastingError, "#{attr_name} is invalid date"
|
11
|
+
end
|
12
|
+
else
|
13
|
+
raise HCast::Errors::CastingError, "#{attr_name} should be a date"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
class HCast::Casters::DateTimeCaster
|
2
|
+
|
3
|
+
def self.cast(value, attr_name, options = {})
|
4
|
+
if value.is_a?(DateTime)
|
5
|
+
value
|
6
|
+
elsif value.is_a?(Time)
|
7
|
+
value.to_datetime
|
8
|
+
elsif value.is_a?(String)
|
9
|
+
begin
|
10
|
+
DateTime.parse(value)
|
11
|
+
rescue ArgumentError => e
|
12
|
+
raise HCast::Errors::CastingError, "#{attr_name} is invalid datetime"
|
13
|
+
end
|
14
|
+
else
|
15
|
+
raise HCast::Errors::CastingError, "#{attr_name} should be a datetime"
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
class HCast::Casters::FloatCaster
|
2
|
+
|
3
|
+
def self.cast(value, attr_name, options = {})
|
4
|
+
if value.is_a?(Float)
|
5
|
+
value
|
6
|
+
elsif value.is_a?(String)
|
7
|
+
begin
|
8
|
+
Float(value)
|
9
|
+
rescue ArgumentError => e
|
10
|
+
raise HCast::Errors::CastingError, "#{attr_name} is invalid float"
|
11
|
+
end
|
12
|
+
else
|
13
|
+
raise HCast::Errors::CastingError, "#{attr_name} should be a float"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
class HCast::Casters::IntegerCaster
|
2
|
+
|
3
|
+
def self.cast(value, attr_name, options = {})
|
4
|
+
if value.is_a?(Integer)
|
5
|
+
value
|
6
|
+
elsif value.is_a?(String)
|
7
|
+
begin
|
8
|
+
Integer(value)
|
9
|
+
rescue ArgumentError => e
|
10
|
+
raise HCast::Errors::CastingError, "#{attr_name} is invalid integer"
|
11
|
+
end
|
12
|
+
else
|
13
|
+
raise HCast::Errors::CastingError, "#{attr_name} should be a integer"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
class HCast::Casters::StringCaster
|
2
|
+
|
3
|
+
def self.cast(value, attr_name, options = {})
|
4
|
+
if value.is_a?(String)
|
5
|
+
value
|
6
|
+
elsif value.is_a?(Symbol)
|
7
|
+
value.to_s
|
8
|
+
else
|
9
|
+
raise HCast::Errors::CastingError, "#{attr_name} should be a string"
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
class HCast::Casters::SymbolCaster
|
2
|
+
MAX_SYMBOL_LENGTH = 1000
|
3
|
+
|
4
|
+
def self.cast(value, attr_name, options = {})
|
5
|
+
if value.is_a?(Symbol)
|
6
|
+
value
|
7
|
+
elsif value.is_a?(String)
|
8
|
+
if value.length > MAX_SYMBOL_LENGTH
|
9
|
+
raise HCast::Errors::CastingError, "#{attr_name} is too long to be a symbol"
|
10
|
+
else
|
11
|
+
value.to_sym
|
12
|
+
end
|
13
|
+
else
|
14
|
+
raise HCast::Errors::CastingError, "#{attr_name} should be a symbol"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
class HCast::Casters::TimeCaster
|
2
|
+
|
3
|
+
def self.cast(value, attr_name, options = {})
|
4
|
+
if value.is_a?(Time)
|
5
|
+
value
|
6
|
+
elsif value.is_a?(String)
|
7
|
+
begin
|
8
|
+
Time.parse(value)
|
9
|
+
rescue ArgumentError => e
|
10
|
+
raise HCast::Errors::CastingError, "#{attr_name} is invalid time"
|
11
|
+
end
|
12
|
+
else
|
13
|
+
raise HCast::Errors::CastingError, "#{attr_name} should be a time"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
@@ -0,0 +1,136 @@
|
|
1
|
+
module HCast
|
2
|
+
# Copied from here https://github.com/rails/rails/blob/master/activesupport/lib/active_support/concern.rb
|
3
|
+
#
|
4
|
+
# A typical module looks like this:
|
5
|
+
#
|
6
|
+
# module M
|
7
|
+
# def self.included(base)
|
8
|
+
# base.extend ClassMethods
|
9
|
+
# base.class_eval do
|
10
|
+
# scope :disabled, -> { where(disabled: true) }
|
11
|
+
# end
|
12
|
+
# end
|
13
|
+
#
|
14
|
+
# module ClassMethods
|
15
|
+
# ...
|
16
|
+
# end
|
17
|
+
# end
|
18
|
+
#
|
19
|
+
# By using <tt>ActiveSupport::Concern</tt> the above module could instead be
|
20
|
+
# written as:
|
21
|
+
#
|
22
|
+
# require 'active_support/concern'
|
23
|
+
#
|
24
|
+
# module M
|
25
|
+
# extend ActiveSupport::Concern
|
26
|
+
#
|
27
|
+
# included do
|
28
|
+
# scope :disabled, -> { where(disabled: true) }
|
29
|
+
# end
|
30
|
+
#
|
31
|
+
# module ClassMethods
|
32
|
+
# ...
|
33
|
+
# end
|
34
|
+
# end
|
35
|
+
#
|
36
|
+
# Moreover, it gracefully handles module dependencies. Given a +Foo+ module
|
37
|
+
# and a +Bar+ module which depends on the former, we would typically write the
|
38
|
+
# following:
|
39
|
+
#
|
40
|
+
# module Foo
|
41
|
+
# def self.included(base)
|
42
|
+
# base.class_eval do
|
43
|
+
# def self.method_injected_by_foo
|
44
|
+
# ...
|
45
|
+
# end
|
46
|
+
# end
|
47
|
+
# end
|
48
|
+
# end
|
49
|
+
#
|
50
|
+
# module Bar
|
51
|
+
# def self.included(base)
|
52
|
+
# base.method_injected_by_foo
|
53
|
+
# end
|
54
|
+
# end
|
55
|
+
#
|
56
|
+
# class Host
|
57
|
+
# include Foo # We need to include this dependency for Bar
|
58
|
+
# include Bar # Bar is the module that Host really needs
|
59
|
+
# end
|
60
|
+
#
|
61
|
+
# But why should +Host+ care about +Bar+'s dependencies, namely +Foo+? We
|
62
|
+
# could try to hide these from +Host+ directly including +Foo+ in +Bar+:
|
63
|
+
#
|
64
|
+
# module Bar
|
65
|
+
# include Foo
|
66
|
+
# def self.included(base)
|
67
|
+
# base.method_injected_by_foo
|
68
|
+
# end
|
69
|
+
# end
|
70
|
+
#
|
71
|
+
# class Host
|
72
|
+
# include Bar
|
73
|
+
# end
|
74
|
+
#
|
75
|
+
# Unfortunately this won't work, since when +Foo+ is included, its <tt>base</tt>
|
76
|
+
# is the +Bar+ module, not the +Host+ class. With <tt>ActiveSupport::Concern</tt>,
|
77
|
+
# module dependencies are properly resolved:
|
78
|
+
#
|
79
|
+
# require 'active_support/concern'
|
80
|
+
#
|
81
|
+
# module Foo
|
82
|
+
# extend ActiveSupport::Concern
|
83
|
+
# included do
|
84
|
+
# def self.method_injected_by_foo
|
85
|
+
# ...
|
86
|
+
# end
|
87
|
+
# end
|
88
|
+
# end
|
89
|
+
#
|
90
|
+
# module Bar
|
91
|
+
# extend ActiveSupport::Concern
|
92
|
+
# include Foo
|
93
|
+
#
|
94
|
+
# included do
|
95
|
+
# self.method_injected_by_foo
|
96
|
+
# end
|
97
|
+
# end
|
98
|
+
#
|
99
|
+
# class Host
|
100
|
+
# include Bar # works, Bar takes care now of its dependencies
|
101
|
+
# end
|
102
|
+
module Concern
|
103
|
+
class MultipleIncludedBlocks < StandardError #:nodoc:
|
104
|
+
def initialize
|
105
|
+
super "Cannot define multiple 'included' blocks for a Concern"
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def self.extended(base) #:nodoc:
|
110
|
+
base.instance_variable_set(:@_dependencies, [])
|
111
|
+
end
|
112
|
+
|
113
|
+
def append_features(base)
|
114
|
+
if base.instance_variable_defined?(:@_dependencies)
|
115
|
+
base.instance_variable_get(:@_dependencies) << self
|
116
|
+
return false
|
117
|
+
else
|
118
|
+
return false if base < self
|
119
|
+
@_dependencies.each { |dep| base.send(:include, dep) }
|
120
|
+
super
|
121
|
+
base.extend const_get(:ClassMethods) if const_defined?(:ClassMethods)
|
122
|
+
base.class_eval(&@_included_block) if instance_variable_defined?(:@_included_block)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def included(base = nil, &block)
|
127
|
+
if base.nil?
|
128
|
+
raise MultipleIncludedBlocks if instance_variable_defined?(:@_included_block)
|
129
|
+
|
130
|
+
@_included_block = block
|
131
|
+
else
|
132
|
+
super
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
data/lib/hcast/config.rb
ADDED
data/lib/hcast/errors.rb
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
module HCast::Errors
|
2
|
+
|
3
|
+
# Base error class for all HCast errors
|
4
|
+
class HCastError < StandardError; end
|
5
|
+
|
6
|
+
# Raised when caster with given name is not registered in HCast
|
7
|
+
class CasterNotFoundError < HCastError; end
|
8
|
+
|
9
|
+
# Raised when some of the given to HCast argument is not valid
|
10
|
+
class ArgumentError < HCastError; end
|
11
|
+
|
12
|
+
# Raised when hash attribute can't be casted
|
13
|
+
class CastingError < HCastError; end
|
14
|
+
|
15
|
+
# Raised when required hash attribute wasn't given for casting
|
16
|
+
class MissingAttributeError < HCastError; end
|
17
|
+
|
18
|
+
# Raised when unexpected hash attribute was given for casting
|
19
|
+
class UnexpectedAttributeError < HCastError; end
|
20
|
+
|
21
|
+
# Raised when hash has validation errors
|
22
|
+
class ValidationError < StandardError
|
23
|
+
attr_reader :errors
|
24
|
+
|
25
|
+
def initialize(message, errors)
|
26
|
+
@errors = errors
|
27
|
+
super(message)
|
28
|
+
end
|
29
|
+
|
30
|
+
def message
|
31
|
+
"#{@message}\n#{errors.to_hash}"
|
32
|
+
end
|
33
|
+
|
34
|
+
def short_message
|
35
|
+
'Validation error'
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module HCast::Metadata
|
2
|
+
class Attribute
|
3
|
+
attr_reader :name, :caster, :options
|
4
|
+
attr_accessor :children
|
5
|
+
|
6
|
+
def initialize(name, caster, options)
|
7
|
+
@name = name
|
8
|
+
@caster = caster
|
9
|
+
@options = options
|
10
|
+
@children = []
|
11
|
+
end
|
12
|
+
|
13
|
+
def has_children?
|
14
|
+
!children.empty?
|
15
|
+
end
|
16
|
+
|
17
|
+
def required?
|
18
|
+
!optional?
|
19
|
+
end
|
20
|
+
|
21
|
+
def optional?
|
22
|
+
options[:optional]
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,339 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe HCast::Caster do
|
4
|
+
describe "#cast" do
|
5
|
+
|
6
|
+
class ContactCaster
|
7
|
+
include HCast::Caster
|
8
|
+
|
9
|
+
attributes do
|
10
|
+
hash :contact do
|
11
|
+
string :name
|
12
|
+
integer :age, optional: true
|
13
|
+
float :weight
|
14
|
+
date :birthday
|
15
|
+
datetime :last_logged_in
|
16
|
+
time :last_visited_at
|
17
|
+
hash :company do
|
18
|
+
string :name
|
19
|
+
end
|
20
|
+
array :emails, each: :string
|
21
|
+
array :social_accounts, each: :hash do
|
22
|
+
string :name
|
23
|
+
symbol :type
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
it "should cast hash attributes" do
|
30
|
+
input_hash = {
|
31
|
+
contact: {
|
32
|
+
name: "John Smith",
|
33
|
+
age: "22",
|
34
|
+
weight: "65.5",
|
35
|
+
birthday: "2014-02-02",
|
36
|
+
last_logged_in: "2014-02-02 10:10:00",
|
37
|
+
last_visited_at: "2014-02-02 10:10:00",
|
38
|
+
company: {
|
39
|
+
name: "MyCo",
|
40
|
+
},
|
41
|
+
emails: [ "test@example.com", "test2@example.com" ],
|
42
|
+
social_accounts: [
|
43
|
+
{
|
44
|
+
name: "john_smith",
|
45
|
+
type: 'twitter',
|
46
|
+
},
|
47
|
+
{
|
48
|
+
name: "John",
|
49
|
+
type: :facebook,
|
50
|
+
},
|
51
|
+
]
|
52
|
+
}
|
53
|
+
}
|
54
|
+
|
55
|
+
casted_hash = ContactCaster.cast(input_hash)
|
56
|
+
|
57
|
+
casted_hash.should == {
|
58
|
+
contact: {
|
59
|
+
name: "John Smith",
|
60
|
+
age: 22,
|
61
|
+
weight: 65.5,
|
62
|
+
birthday: Date.parse("2014-02-02"),
|
63
|
+
last_logged_in: DateTime.parse("2014-02-02 10:10:00"),
|
64
|
+
last_visited_at: Time.parse("2014-02-02 10:10:00"),
|
65
|
+
company: {
|
66
|
+
name: "MyCo",
|
67
|
+
},
|
68
|
+
emails: [ "test@example.com", "test2@example.com" ],
|
69
|
+
social_accounts: [
|
70
|
+
{
|
71
|
+
name: "john_smith",
|
72
|
+
type: :twitter,
|
73
|
+
},
|
74
|
+
{
|
75
|
+
name: "John",
|
76
|
+
type: :facebook,
|
77
|
+
},
|
78
|
+
]
|
79
|
+
}
|
80
|
+
}
|
81
|
+
end
|
82
|
+
|
83
|
+
it "should raise error if some attribute can't be casted" do
|
84
|
+
input_hash = {
|
85
|
+
contact: {
|
86
|
+
name: {},
|
87
|
+
age: 22,
|
88
|
+
weight: 65.5,
|
89
|
+
birthday: Date.today,
|
90
|
+
last_logged_in: DateTime.now,
|
91
|
+
last_visited_at: Time.now,
|
92
|
+
company: {
|
93
|
+
name: "MyCo",
|
94
|
+
},
|
95
|
+
emails: [ "test@example.com", "test2@example.com" ],
|
96
|
+
social_accounts: [
|
97
|
+
{
|
98
|
+
name: "john_smith",
|
99
|
+
type: :twitter,
|
100
|
+
},
|
101
|
+
{
|
102
|
+
name: "John",
|
103
|
+
type: :facebook,
|
104
|
+
}
|
105
|
+
]
|
106
|
+
}
|
107
|
+
}
|
108
|
+
|
109
|
+
expect do
|
110
|
+
ContactCaster.cast(input_hash)
|
111
|
+
end.to raise_error(HCast::Errors::CastingError, "name should be a string")
|
112
|
+
end
|
113
|
+
|
114
|
+
it "should raise error if some attribute wasn't given" do
|
115
|
+
input_hash = {
|
116
|
+
contact: {
|
117
|
+
age: 22,
|
118
|
+
weight: 65.5,
|
119
|
+
birthday: Date.today,
|
120
|
+
last_logged_in: DateTime.now,
|
121
|
+
last_visited_at: Time.now,
|
122
|
+
company: {
|
123
|
+
name: "MyCo",
|
124
|
+
},
|
125
|
+
emails: [ "test@example.com", "test2@example.com" ],
|
126
|
+
social_accounts: [
|
127
|
+
{
|
128
|
+
name: "john_smith",
|
129
|
+
type: :twitter,
|
130
|
+
},
|
131
|
+
{
|
132
|
+
name: "John",
|
133
|
+
type: :facebook,
|
134
|
+
}
|
135
|
+
]
|
136
|
+
}
|
137
|
+
}
|
138
|
+
|
139
|
+
expect do
|
140
|
+
ContactCaster.cast(input_hash)
|
141
|
+
end.to raise_error(HCast::Errors::MissingAttributeError, "name should be given")
|
142
|
+
end
|
143
|
+
|
144
|
+
it "should not raise error if attribute is optional" do
|
145
|
+
input_hash = {
|
146
|
+
contact: {
|
147
|
+
name: "Jim",
|
148
|
+
weight: 65.5,
|
149
|
+
birthday: Date.today,
|
150
|
+
last_logged_in: DateTime.now,
|
151
|
+
last_visited_at: Time.now,
|
152
|
+
company: {
|
153
|
+
name: "MyCo",
|
154
|
+
},
|
155
|
+
emails: [ "test@example.com", "test2@example.com" ],
|
156
|
+
social_accounts: [
|
157
|
+
{
|
158
|
+
name: "john_smith",
|
159
|
+
type: :twitter,
|
160
|
+
},
|
161
|
+
{
|
162
|
+
name: "John",
|
163
|
+
type: :facebook,
|
164
|
+
},
|
165
|
+
]
|
166
|
+
}
|
167
|
+
}
|
168
|
+
|
169
|
+
expect do
|
170
|
+
ContactCaster.cast(input_hash)
|
171
|
+
end.to_not raise_error
|
172
|
+
end
|
173
|
+
|
174
|
+
it "should raise error if unexpected attribute was given" do
|
175
|
+
input_hash = {
|
176
|
+
wrong_attribute: 'foo',
|
177
|
+
contact: {
|
178
|
+
name: "Jim",
|
179
|
+
weight: 65.5,
|
180
|
+
birthday: Date.today,
|
181
|
+
last_logged_in: DateTime.now,
|
182
|
+
last_visited_at: Time.now,
|
183
|
+
company: {
|
184
|
+
name: "MyCo",
|
185
|
+
},
|
186
|
+
emails: [ "test@example.com", "test2@example.com" ],
|
187
|
+
social_accounts: [
|
188
|
+
{
|
189
|
+
name: "john_smith",
|
190
|
+
type: :twitter,
|
191
|
+
},
|
192
|
+
{
|
193
|
+
name: "John",
|
194
|
+
type: :facebook,
|
195
|
+
},
|
196
|
+
]
|
197
|
+
}
|
198
|
+
}
|
199
|
+
|
200
|
+
expect do
|
201
|
+
ContactCaster.cast(input_hash)
|
202
|
+
end.to raise_error(HCast::Errors::UnexpectedAttributeError, "Unexpected attributes given: [:wrong_attribute]")
|
203
|
+
end
|
204
|
+
|
205
|
+
it "should convert accept hash with string keys and cast them to symbol keys" do
|
206
|
+
input_hash = {
|
207
|
+
'contact' => {
|
208
|
+
'name' => "John Smith",
|
209
|
+
'age' => "22",
|
210
|
+
'weight' => "65.5",
|
211
|
+
'birthday' => "2014-02-02",
|
212
|
+
'last_logged_in' => "2014-02-02 10:10:00",
|
213
|
+
'last_visited_at' => "2014-02-02 10:10:00",
|
214
|
+
'company' => {
|
215
|
+
'name' => "MyCo",
|
216
|
+
},
|
217
|
+
'emails' => [ "test@example.com", "test2@example.com" ],
|
218
|
+
'social_accounts' => [
|
219
|
+
{
|
220
|
+
'name' => "john_smith",
|
221
|
+
'type' => 'twitter',
|
222
|
+
},
|
223
|
+
{
|
224
|
+
'name' => "John",
|
225
|
+
'type' => :facebook,
|
226
|
+
},
|
227
|
+
]
|
228
|
+
}
|
229
|
+
}
|
230
|
+
|
231
|
+
casted_hash = ContactCaster.cast(input_hash, input_keys: :string, output_keys: :symbol)
|
232
|
+
|
233
|
+
casted_hash.should == {
|
234
|
+
contact: {
|
235
|
+
name: "John Smith",
|
236
|
+
age: 22,
|
237
|
+
weight: 65.5,
|
238
|
+
birthday: Date.parse("2014-02-02"),
|
239
|
+
last_logged_in: DateTime.parse("2014-02-02 10:10:00"),
|
240
|
+
last_visited_at: Time.parse("2014-02-02 10:10:00"),
|
241
|
+
company: {
|
242
|
+
name: "MyCo",
|
243
|
+
},
|
244
|
+
emails: [ "test@example.com", "test2@example.com" ],
|
245
|
+
social_accounts: [
|
246
|
+
{
|
247
|
+
name: "john_smith",
|
248
|
+
type: :twitter,
|
249
|
+
},
|
250
|
+
{
|
251
|
+
name: "John",
|
252
|
+
type: :facebook,
|
253
|
+
},
|
254
|
+
]
|
255
|
+
}
|
256
|
+
}
|
257
|
+
end
|
258
|
+
end
|
259
|
+
|
260
|
+
context "checking invalid parameters" do
|
261
|
+
it "should raise CaterNotFound exception if caster name is invalid" do
|
262
|
+
expect do
|
263
|
+
class WrongCaster
|
264
|
+
include HCast::Caster
|
265
|
+
|
266
|
+
attributes do
|
267
|
+
integr :name
|
268
|
+
end
|
269
|
+
end
|
270
|
+
end.to raise_error(HCast::Errors::CasterNotFoundError)
|
271
|
+
end
|
272
|
+
end
|
273
|
+
|
274
|
+
context "validations" do
|
275
|
+
before :all do
|
276
|
+
class ContactWithValidationsCaster
|
277
|
+
include HCast::Caster
|
278
|
+
|
279
|
+
attributes do
|
280
|
+
hash :contact do
|
281
|
+
string :name, presence: true, length: { max: 5 }
|
282
|
+
integer :age, optional: true
|
283
|
+
float :weight, numericality: { less_than_or_equal_to: 200 }
|
284
|
+
date :birthday
|
285
|
+
datetime :last_logged_in
|
286
|
+
time :last_visited_at
|
287
|
+
hash :company do
|
288
|
+
string :name, length: { min: 2 }
|
289
|
+
end
|
290
|
+
array :emails, each: :string
|
291
|
+
array :social_accounts, each: :hash do
|
292
|
+
string :name
|
293
|
+
symbol :type, inclusion: { in: [:twitter, :facebook] }
|
294
|
+
end
|
295
|
+
end
|
296
|
+
end
|
297
|
+
end
|
298
|
+
end
|
299
|
+
|
300
|
+
it "should collect validation errors and raise exception when hash is invalid" do
|
301
|
+
begin
|
302
|
+
ContactWithValidationsCaster.cast(
|
303
|
+
contact: {
|
304
|
+
name: "John Smith",
|
305
|
+
age: "22",
|
306
|
+
weight: "65.5",
|
307
|
+
birthday: "2014-02-02",
|
308
|
+
last_logged_in: "2014-02-02 10:10:00",
|
309
|
+
last_visited_at: "2014-02-02 10:10:00",
|
310
|
+
company: {
|
311
|
+
name: "MyCo",
|
312
|
+
},
|
313
|
+
emails: [ "test@example.com", "test2@example.com" ],
|
314
|
+
social_accounts: [
|
315
|
+
{
|
316
|
+
name: "john_smith",
|
317
|
+
type: 'twitter',
|
318
|
+
},
|
319
|
+
{
|
320
|
+
name: "John",
|
321
|
+
type: :yahoo,
|
322
|
+
},
|
323
|
+
]
|
324
|
+
}
|
325
|
+
)
|
326
|
+
rescue HCast::Errors::ValidationError => e
|
327
|
+
e.errors.to_hash.should == {
|
328
|
+
contact: {
|
329
|
+
name: ["can't be more than 5"],
|
330
|
+
social_accounts: [
|
331
|
+
{},
|
332
|
+
{ type: ["should be included in [:twitter, :facebook]"] },
|
333
|
+
]
|
334
|
+
}
|
335
|
+
}
|
336
|
+
end
|
337
|
+
end
|
338
|
+
end
|
339
|
+
end
|