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.
@@ -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