hcast 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,11 @@
1
+ class HCast::Casters::HashCaster
2
+
3
+ def self.cast(value, attr_name, options = {})
4
+ if value.is_a?(Hash)
5
+ value
6
+ else
7
+ raise HCast::Errors::CastingError, "#{attr_name} should be a hash"
8
+ end
9
+ end
10
+
11
+ 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
@@ -0,0 +1,11 @@
1
+ class HCast::Config
2
+ attr_accessor :input_keys, :output_keys
3
+
4
+ def input_keys
5
+ @input_keys || :symbol
6
+ end
7
+
8
+ def output_keys
9
+ @output_keys || :symbol
10
+ end
11
+ end
@@ -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,3 @@
1
+ module HCast
2
+ VERSION = "0.0.1"
3
+ 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