hashcast 0.4.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 +7 -0
- data/.gitignore +8 -0
- data/.travis.yml +3 -0
- data/Gemfile +7 -0
- data/Gemfile.lock +50 -0
- data/LICENSE.txt +22 -0
- data/README.md +125 -0
- data/Rakefile +1 -0
- data/benchmark/benchmark.rb +66 -0
- data/benchmark/casters.rb +50 -0
- data/docs/_config.yml +1 -0
- data/docs/index.md +124 -0
- data/hcast.gemspec +27 -0
- data/lib/hashcast.rb +46 -0
- data/lib/hashcast/attributes_caster.rb +97 -0
- data/lib/hashcast/attributes_parser.rb +63 -0
- data/lib/hashcast/caster.rb +144 -0
- data/lib/hashcast/casters.rb +13 -0
- data/lib/hashcast/casters/array_caster.rb +27 -0
- data/lib/hashcast/casters/boolean_caster.rb +13 -0
- data/lib/hashcast/casters/date_caster.rb +15 -0
- data/lib/hashcast/casters/datetime_caster.rb +15 -0
- data/lib/hashcast/casters/float_caster.rb +14 -0
- data/lib/hashcast/casters/hash_caster.rb +8 -0
- data/lib/hashcast/casters/integer_caster.rb +15 -0
- data/lib/hashcast/casters/string_caster.rb +8 -0
- data/lib/hashcast/casters/symbol_caster.rb +15 -0
- data/lib/hashcast/casters/time_caster.rb +15 -0
- data/lib/hashcast/concern.rb +136 -0
- data/lib/hashcast/config.rb +11 -0
- data/lib/hashcast/errors.rb +49 -0
- data/lib/hashcast/metadata/attribute.rb +30 -0
- data/lib/hashcast/version.rb +3 -0
- data/spec/hcast/caster_spec.rb +471 -0
- data/spec/hcast/casters_spec.rb +245 -0
- data/spec/hcast/hcast_spec.rb +37 -0
- data/spec/spec_helper.rb +21 -0
- metadata +169 -0
@@ -0,0 +1,15 @@
|
|
1
|
+
class HashCast::Casters::DateTimeCaster
|
2
|
+
|
3
|
+
def self.cast(value, attr_name, options = {})
|
4
|
+
return value if value.is_a?(DateTime)
|
5
|
+
return value.to_datetime if value.is_a?(Time)
|
6
|
+
return cast_string(value) if value.is_a?(String)
|
7
|
+
raise HashCast::Errors::CastingError, "should be a datetime"
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.cast_string(value)
|
11
|
+
DateTime.parse(value)
|
12
|
+
rescue ArgumentError => e
|
13
|
+
raise HashCast::Errors::CastingError, "is invalid datetime"
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
class HashCast::Casters::FloatCaster
|
2
|
+
|
3
|
+
def self.cast(value, attr_name, options = {})
|
4
|
+
return value if value.is_a?(Float)
|
5
|
+
return cast_string(value) if value.is_a?(String)
|
6
|
+
raise HashCast::Errors::CastingError, "#{value} should be a float"
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.cast_string(value)
|
10
|
+
Float(value)
|
11
|
+
rescue ArgumentError => e
|
12
|
+
raise HashCast::Errors::CastingError, "#{value} is invalid float"
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
class HashCast::Casters::IntegerCaster
|
2
|
+
|
3
|
+
def self.cast(value, attr_name, options = {})
|
4
|
+
return value if value.is_a?(Integer)
|
5
|
+
return cast_string(value) if value.is_a?(String)
|
6
|
+
raise HashCast::Errors::CastingError, "should be a integer"
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.cast_string(value)
|
10
|
+
Integer(value)
|
11
|
+
rescue ArgumentError => e
|
12
|
+
raise HashCast::Errors::CastingError, "is invalid integer"
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
class HashCast::Casters::SymbolCaster
|
2
|
+
MAX_SYMBOL_LENGTH = 1000
|
3
|
+
|
4
|
+
def self.cast(value, attr_name, options = {})
|
5
|
+
return value if value.is_a?(Symbol)
|
6
|
+
return cast_string(value) if value.is_a?(String)
|
7
|
+
raise HashCast::Errors::CastingError, "should be a symbol"
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.cast_string(value)
|
11
|
+
return value.to_sym if value.length <= MAX_SYMBOL_LENGTH
|
12
|
+
raise HashCast::Errors::CastingError, "is too long to be a symbol (#{MAX_SYMBOL_LENGTH} max.)"
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
class HashCast::Casters::TimeCaster
|
2
|
+
|
3
|
+
def self.cast(value, attr_name, options = {})
|
4
|
+
return value if value.is_a?(Time)
|
5
|
+
return cast_string(value) if value.is_a?(String)
|
6
|
+
raise HashCast::Errors::CastingError, "#{value} should be a time"
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.cast_string(value)
|
10
|
+
Time.parse(value)
|
11
|
+
rescue ArgumentError => e
|
12
|
+
raise HashCast::Errors::CastingError, "#{value} is invalid time"
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
@@ -0,0 +1,136 @@
|
|
1
|
+
module HashCast
|
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
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module HashCast::Errors
|
2
|
+
|
3
|
+
# Base error class for all HashCast errors
|
4
|
+
class HashCastError < StandardError; end
|
5
|
+
|
6
|
+
# Raised when caster with given name is not registered in HashCast
|
7
|
+
class CasterNotFoundError < HashCastError; end
|
8
|
+
|
9
|
+
# Raised when some of the given to HashCast argument is not valid
|
10
|
+
class ArgumentError < HashCastError; end
|
11
|
+
|
12
|
+
class AttributeError < HashCastError
|
13
|
+
attr_reader :namespaces
|
14
|
+
|
15
|
+
def initialize(message, namespace = nil)
|
16
|
+
super(message)
|
17
|
+
@namespaces = []
|
18
|
+
@namespaces << namespace if namespace
|
19
|
+
end
|
20
|
+
|
21
|
+
def add_namespace(namespace)
|
22
|
+
namespaces << namespace
|
23
|
+
end
|
24
|
+
|
25
|
+
def message
|
26
|
+
to_s
|
27
|
+
end
|
28
|
+
|
29
|
+
def to_s
|
30
|
+
if namespaces.empty?
|
31
|
+
super
|
32
|
+
else
|
33
|
+
reverted_namespaces = namespaces.reverse
|
34
|
+
msg = reverted_namespaces.first.to_s
|
35
|
+
msg += reverted_namespaces[1..-1].inject("") { |res, item| res += "[#{item}]"}
|
36
|
+
msg + " " + super
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
# Raised when hash attribute can't be casted
|
42
|
+
class CastingError < AttributeError; end
|
43
|
+
|
44
|
+
# Raised when required hash attribute wasn't given for casting
|
45
|
+
class MissingAttributeError < AttributeError; end
|
46
|
+
|
47
|
+
# Raised when unexpected hash attribute was given for casting
|
48
|
+
class UnexpectedAttributeError < AttributeError; end
|
49
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module HashCast::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
|
+
def allow_nil?
|
26
|
+
!!options[:allow_nil]
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,471 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe HashCast::Caster do
|
4
|
+
describe "#cast" do
|
5
|
+
|
6
|
+
class ContactCaster
|
7
|
+
include HashCast::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
|
+
describe "Custom casters" do
|
84
|
+
class SettingsCaster
|
85
|
+
include HashCast::Caster
|
86
|
+
|
87
|
+
attributes do
|
88
|
+
string :account
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
class EmailCaster
|
93
|
+
include HashCast::Caster
|
94
|
+
|
95
|
+
attributes do
|
96
|
+
string :address
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
class CompanyCaster
|
101
|
+
include HashCast::Caster
|
102
|
+
|
103
|
+
attributes do
|
104
|
+
string :name
|
105
|
+
hash :settings, caster: SettingsCaster
|
106
|
+
array :emails, caster: EmailCaster
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
it "should allow specify caster for nested hash attribute" do
|
111
|
+
casted_hash = CompanyCaster.cast(
|
112
|
+
name: 'Might & Magic',
|
113
|
+
settings: {
|
114
|
+
account: :'migthy_lord'
|
115
|
+
},
|
116
|
+
emails: [
|
117
|
+
{ address: :'test1@example.com' },
|
118
|
+
{ address: :'test2@example.com' },
|
119
|
+
]
|
120
|
+
)
|
121
|
+
|
122
|
+
casted_hash.should == {
|
123
|
+
name: "Might & Magic",
|
124
|
+
settings: { account: "migthy_lord" },
|
125
|
+
emails: [
|
126
|
+
{ address: "test1@example.com" },
|
127
|
+
{ address: "test2@example.com" }
|
128
|
+
]
|
129
|
+
}
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
it "should raise error if some attribute can't be casted" do
|
134
|
+
input_hash = {
|
135
|
+
contact: {
|
136
|
+
name: {},
|
137
|
+
age: 22,
|
138
|
+
weight: 65.5,
|
139
|
+
birthday: Date.today,
|
140
|
+
last_logged_in: DateTime.now,
|
141
|
+
last_visited_at: Time.now,
|
142
|
+
company: {
|
143
|
+
name: "MyCo",
|
144
|
+
},
|
145
|
+
emails: [ "test@example.com", "test2@example.com" ],
|
146
|
+
social_accounts: [
|
147
|
+
{
|
148
|
+
name: "john_smith",
|
149
|
+
type: :twitter,
|
150
|
+
},
|
151
|
+
{
|
152
|
+
name: "John",
|
153
|
+
type: :facebook,
|
154
|
+
}
|
155
|
+
]
|
156
|
+
}
|
157
|
+
}
|
158
|
+
|
159
|
+
expect do
|
160
|
+
ContactCaster.cast(input_hash)
|
161
|
+
end.to raise_error(HashCast::Errors::CastingError, "contact[name] should be a string, but was Hash")
|
162
|
+
end
|
163
|
+
|
164
|
+
it "should raise error if some attribute wasn't given" do
|
165
|
+
input_hash = {
|
166
|
+
contact: {
|
167
|
+
age: 22,
|
168
|
+
weight: 65.5,
|
169
|
+
birthday: Date.today,
|
170
|
+
last_logged_in: DateTime.now,
|
171
|
+
last_visited_at: Time.now,
|
172
|
+
company: {
|
173
|
+
name: "MyCo",
|
174
|
+
},
|
175
|
+
emails: [ "test@example.com", "test2@example.com" ],
|
176
|
+
social_accounts: [
|
177
|
+
{
|
178
|
+
name: "john_smith",
|
179
|
+
type: :twitter,
|
180
|
+
},
|
181
|
+
{
|
182
|
+
name: "John",
|
183
|
+
type: :facebook,
|
184
|
+
}
|
185
|
+
]
|
186
|
+
}
|
187
|
+
}
|
188
|
+
|
189
|
+
expect do
|
190
|
+
ContactCaster.cast(input_hash)
|
191
|
+
end.to raise_error(HashCast::Errors::MissingAttributeError, "contact[name] should be given")
|
192
|
+
end
|
193
|
+
|
194
|
+
it "should not raise error if attribute is optional" do
|
195
|
+
input_hash = {
|
196
|
+
contact: {
|
197
|
+
name: "Jim",
|
198
|
+
weight: 65.5,
|
199
|
+
birthday: Date.today,
|
200
|
+
last_logged_in: DateTime.now,
|
201
|
+
last_visited_at: Time.now,
|
202
|
+
company: {
|
203
|
+
name: "MyCo",
|
204
|
+
},
|
205
|
+
emails: [ "test@example.com", "test2@example.com" ],
|
206
|
+
social_accounts: [
|
207
|
+
{
|
208
|
+
name: "john_smith",
|
209
|
+
type: :twitter,
|
210
|
+
},
|
211
|
+
{
|
212
|
+
name: "John",
|
213
|
+
type: :facebook,
|
214
|
+
},
|
215
|
+
]
|
216
|
+
}
|
217
|
+
}
|
218
|
+
|
219
|
+
expect do
|
220
|
+
ContactCaster.cast(input_hash)
|
221
|
+
end.to_not raise_error
|
222
|
+
end
|
223
|
+
|
224
|
+
it "should raise error if unexpected attribute was given" do
|
225
|
+
input_hash = {
|
226
|
+
contact: {
|
227
|
+
wrong_attribute: 'foo',
|
228
|
+
name: "Jim",
|
229
|
+
weight: 65.5,
|
230
|
+
birthday: Date.today,
|
231
|
+
last_logged_in: DateTime.now,
|
232
|
+
last_visited_at: Time.now,
|
233
|
+
company: {
|
234
|
+
name: "MyCo",
|
235
|
+
},
|
236
|
+
emails: [ "test@example.com", "test2@example.com" ],
|
237
|
+
social_accounts: [
|
238
|
+
{
|
239
|
+
name: "john_smith",
|
240
|
+
type: :twitter,
|
241
|
+
},
|
242
|
+
{
|
243
|
+
name: "John",
|
244
|
+
type: :facebook,
|
245
|
+
},
|
246
|
+
]
|
247
|
+
}
|
248
|
+
}
|
249
|
+
|
250
|
+
expect do
|
251
|
+
ContactCaster.cast(input_hash)
|
252
|
+
end.to raise_error(HashCast::Errors::UnexpectedAttributeError, "contact[wrong_attribute] is not valid attribute name")
|
253
|
+
end
|
254
|
+
|
255
|
+
it "shouldn't unexpected attributes error if skip_unexpected_attributes flag is set to true" do
|
256
|
+
input_hash = {
|
257
|
+
contact: {
|
258
|
+
wrong_attribute: 'foo',
|
259
|
+
name: "Jim",
|
260
|
+
weight: 65.5,
|
261
|
+
birthday: Date.today,
|
262
|
+
last_logged_in: DateTime.now,
|
263
|
+
last_visited_at: Time.now,
|
264
|
+
company: {
|
265
|
+
name: "MyCo",
|
266
|
+
},
|
267
|
+
emails: [ "test@example.com", "test2@example.com" ],
|
268
|
+
social_accounts: [
|
269
|
+
{
|
270
|
+
name: "john_smith",
|
271
|
+
type: :twitter,
|
272
|
+
},
|
273
|
+
{
|
274
|
+
name: "John",
|
275
|
+
type: :facebook,
|
276
|
+
},
|
277
|
+
]
|
278
|
+
}
|
279
|
+
}
|
280
|
+
|
281
|
+
expect do
|
282
|
+
ContactCaster.cast(input_hash, skip_unexpected_attributes: true)
|
283
|
+
end.not_to raise_error(HashCast::Errors::UnexpectedAttributeError)
|
284
|
+
|
285
|
+
end
|
286
|
+
|
287
|
+
it "should convert accept hash with string keys and cast them to symbol keys" do
|
288
|
+
input_hash = {
|
289
|
+
'contact' => {
|
290
|
+
'name' => "John Smith",
|
291
|
+
'age' => "22",
|
292
|
+
'weight' => "65.5",
|
293
|
+
'birthday' => "2014-02-02",
|
294
|
+
'last_logged_in' => "2014-02-02 10:10:00",
|
295
|
+
'last_visited_at' => "2014-02-02 10:10:00",
|
296
|
+
'company' => {
|
297
|
+
'name' => "MyCo",
|
298
|
+
},
|
299
|
+
'emails' => [ "test@example.com", "test2@example.com" ],
|
300
|
+
'social_accounts' => [
|
301
|
+
{
|
302
|
+
'name' => "john_smith",
|
303
|
+
'type' => 'twitter',
|
304
|
+
},
|
305
|
+
{
|
306
|
+
'name' => "John",
|
307
|
+
'type' => :facebook,
|
308
|
+
},
|
309
|
+
]
|
310
|
+
}
|
311
|
+
}
|
312
|
+
|
313
|
+
casted_hash = ContactCaster.cast(input_hash, input_keys: :string, output_keys: :symbol)
|
314
|
+
|
315
|
+
casted_hash.should == {
|
316
|
+
contact: {
|
317
|
+
name: "John Smith",
|
318
|
+
age: 22,
|
319
|
+
weight: 65.5,
|
320
|
+
birthday: Date.parse("2014-02-02"),
|
321
|
+
last_logged_in: DateTime.parse("2014-02-02 10:10:00"),
|
322
|
+
last_visited_at: Time.parse("2014-02-02 10:10:00"),
|
323
|
+
company: {
|
324
|
+
name: "MyCo",
|
325
|
+
},
|
326
|
+
emails: [ "test@example.com", "test2@example.com" ],
|
327
|
+
social_accounts: [
|
328
|
+
{
|
329
|
+
name: "john_smith",
|
330
|
+
type: :twitter,
|
331
|
+
},
|
332
|
+
{
|
333
|
+
name: "John",
|
334
|
+
type: :facebook,
|
335
|
+
},
|
336
|
+
]
|
337
|
+
}
|
338
|
+
}
|
339
|
+
end
|
340
|
+
end
|
341
|
+
|
342
|
+
context "checking invalid parameters" do
|
343
|
+
it "should raise CaterNotFound exception if caster name is invalid" do
|
344
|
+
expect do
|
345
|
+
class WrongCaster
|
346
|
+
include HashCast::Caster
|
347
|
+
|
348
|
+
attributes do
|
349
|
+
integr :name
|
350
|
+
end
|
351
|
+
end
|
352
|
+
end.to raise_error(HashCast::Errors::CasterNotFoundError)
|
353
|
+
end
|
354
|
+
end
|
355
|
+
|
356
|
+
context "allow nil values" do
|
357
|
+
before(:all) do
|
358
|
+
class HomeCaster
|
359
|
+
include HashCast::Caster
|
360
|
+
|
361
|
+
attributes do
|
362
|
+
string :city
|
363
|
+
integer :zip, allow_nil: true
|
364
|
+
end
|
365
|
+
end
|
366
|
+
end
|
367
|
+
|
368
|
+
it "should allow nil values if allow_nil is set to true" do
|
369
|
+
HomeCaster.cast(
|
370
|
+
city: 'Kazan',
|
371
|
+
zip: nil
|
372
|
+
)
|
373
|
+
end
|
374
|
+
|
375
|
+
it "should allow nil values unless allow_nil is set to true" do
|
376
|
+
expect do
|
377
|
+
HomeCaster.cast(
|
378
|
+
city: nil,
|
379
|
+
zip: nil
|
380
|
+
)
|
381
|
+
end.to raise_error(HashCast::Errors::CastingError, "city should be a string, but was NilClass")
|
382
|
+
end
|
383
|
+
end
|
384
|
+
|
385
|
+
context "input_keys" do
|
386
|
+
it "strings -> symbol works" do
|
387
|
+
expect(
|
388
|
+
SettingsCaster.cast({"account" => "value"}, {input_keys: :string, output_keys: :symbol})
|
389
|
+
).to eq({account: "value"})
|
390
|
+
end
|
391
|
+
|
392
|
+
it "symbol -> string works" do
|
393
|
+
expect(
|
394
|
+
SettingsCaster.cast({account: "value"}, {input_keys: :symbol, output_keys: :string})
|
395
|
+
).to eq({"account" => "value"})
|
396
|
+
end
|
397
|
+
|
398
|
+
it "symbol -> symbol works" do
|
399
|
+
expect(
|
400
|
+
SettingsCaster.cast({account: "value"}, {input_keys: :symbol, output_keys: :symbol})
|
401
|
+
).to eq({account: "value"})
|
402
|
+
end
|
403
|
+
|
404
|
+
it "string -> string works" do
|
405
|
+
pending
|
406
|
+
expect(
|
407
|
+
SettingsCaster.cast({"account" => "value"}, {input_keys: :string, output_keys: :string})
|
408
|
+
).to eq({"account" => "value"})
|
409
|
+
end
|
410
|
+
end
|
411
|
+
|
412
|
+
context "possible exeptions" do
|
413
|
+
it "raises when attributes were not defined" do
|
414
|
+
class NoAttrCaster
|
415
|
+
include HashCast::Caster
|
416
|
+
end
|
417
|
+
|
418
|
+
expect{
|
419
|
+
NoAttrCaster.cast({a: 1})
|
420
|
+
}.to raise_error(HashCast::Errors::ArgumentError, "Attributes block should be defined")
|
421
|
+
end
|
422
|
+
|
423
|
+
context "check_options!" do
|
424
|
+
it "raises when options are not a hash" do
|
425
|
+
expect {
|
426
|
+
SettingsCaster.cast({account: "some"}, 1)
|
427
|
+
}.to raise_error(HashCast::Errors::ArgumentError, "Options should be a hash")
|
428
|
+
end
|
429
|
+
|
430
|
+
it "raises on bad options" do
|
431
|
+
expect {
|
432
|
+
SettingsCaster.cast({account: "some"}, {input_keys: "string"})
|
433
|
+
}.to raise_error(HashCast::Errors::ArgumentError, "input_keys should be :string or :symbol")
|
434
|
+
|
435
|
+
expect {
|
436
|
+
SettingsCaster.cast({account: "some"}, {output_keys: "string"})
|
437
|
+
}.to raise_error(HashCast::Errors::ArgumentError, "output_keys should be :string or :symbol")
|
438
|
+
end
|
439
|
+
end
|
440
|
+
|
441
|
+
it "raises when input is not hash" do
|
442
|
+
expect {
|
443
|
+
SettingsCaster.cast(["some"])
|
444
|
+
}.to raise_error(HashCast::Errors::ArgumentError, "Hash should be given")
|
445
|
+
end
|
446
|
+
|
447
|
+
context "AttributesParser" do
|
448
|
+
it "raises when attribute name is not string/symbol" do
|
449
|
+
expect{
|
450
|
+
class BadAttrNameCaster
|
451
|
+
include HashCast::Caster
|
452
|
+
attributes do
|
453
|
+
string 4545
|
454
|
+
end
|
455
|
+
end
|
456
|
+
}.to raise_error(HashCast::Errors::ArgumentError, "attribute name should be a symbol or string")
|
457
|
+
end
|
458
|
+
|
459
|
+
it "raises when attribute options are not hash" do
|
460
|
+
expect{
|
461
|
+
class AttrOptionsNotHashCast
|
462
|
+
include HashCast::Caster
|
463
|
+
attributes do
|
464
|
+
string :some, [1,2,3]
|
465
|
+
end
|
466
|
+
end
|
467
|
+
}.to raise_error(HashCast::Errors::ArgumentError, "attribute options should be a Hash")
|
468
|
+
end
|
469
|
+
end
|
470
|
+
end
|
471
|
+
end
|