hashcast 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,8 @@
1
+ class HashCast::Casters::HashCast
2
+
3
+ def self.cast(value, attr_name, options = {})
4
+ return value if value.is_a?(Hash)
5
+ raise HashCast::Errors::CastingError, "should be a hash"
6
+ end
7
+
8
+ 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,8 @@
1
+ class HashCast::Casters::StringCaster
2
+
3
+ def self.cast(value, attr_name, options = {})
4
+ return value if value.is_a?(String)
5
+ return value.to_s if value.is_a?(Symbol)
6
+ raise HashCast::Errors::CastingError, "should be a string, but was #{value.class}"
7
+ end
8
+ 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,11 @@
1
+ class HashCast::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,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,3 @@
1
+ module HashCast
2
+ VERSION = "0.4.0"
3
+ 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