safe_type 0.0.3 → 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: eca25594ac5b8d10503472a6adc49dba43091c3f
4
- data.tar.gz: c469b2f8539bc78995b69b200757c0c420997b75
3
+ metadata.gz: a67c63237e5cf69ced4be0ee3a485f7ff3fba588
4
+ data.tar.gz: f92d5b559d4a2cbf322187f2f4eef1d17d7967a5
5
5
  SHA512:
6
- metadata.gz: 8f56662fd4b888c637db732c23d9248d14691af30d95499fd73e511d1cff836c5816f22b816960c9c0bcbb65aecc08f7a11f037cb9023a762d58b5bb925b3e3d
7
- data.tar.gz: 4b3bc19ab41437031109d8922a9187623a5912e0dd295ab3461e26c80a44e98b5f031d0a4c15e14e1da7f552d401ab60589c14bac1cc0c5a692493b5fa3f76e9
6
+ metadata.gz: 6fab8fcf295c05a748678d0ed5f4770797f8b83c3d637fe5e70eedb0fd62f405f8703577a4dd2cfb7f45efb515437c8bc7064f6f993c945a2dc80c5d84db519d
7
+ data.tar.gz: 311fe81e529e0f948cbefadf8d2efea6cd992897a692468c253579f50ad2606f663c890bad05d22037da850a55d76abf4dcd46ef6a879fc503b2bd527ea3f756
data/README.md CHANGED
@@ -1,16 +1,17 @@
1
- # SafeType
1
+ SafeType
2
+ ======
2
3
  [![Gem Version](https://badge.fury.io/rb/safe_type.svg)](https://badge.fury.io/rb/safe_type)
3
4
  [![Build Status](https://travis-ci.org/chanzuckerberg/safe_type.svg?branch=master)](https://travis-ci.org/chanzuckerberg/safe_type)
4
5
  [![Maintainability](https://api.codeclimate.com/v1/badges/7fbc9a4038b86ef639e1/maintainability)](https://codeclimate.com/github/chanzuckerberg/safe_type/maintainability)
5
6
  [![Test Coverage](https://api.codeclimate.com/v1/badges/7fbc9a4038b86ef639e1/test_coverage)](https://codeclimate.com/github/chanzuckerberg/safe_type/test_coverage)
6
7
 
7
- While working with environment variables, routing parameters, JSON objects,
8
+ While working with environment variables, routing parameters, API responses,
8
9
  or other Hash-like objects require parsing,
9
- we often require type coercion to assure expected behaviors.
10
+ we often need type coercion to assure expected behaviors.
10
11
 
11
12
  ***SafeType*** provides an intuitive type coercion interface and type enhancement.
12
13
 
13
- ## Install
14
+ # Install
14
15
 
15
16
  We can install `safe_type` using `gem install`:
16
17
 
@@ -24,118 +25,158 @@ Or we can add it as a dependency in the `Gemfile` and run `bundle install`:
24
25
  gem 'safe_type'
25
26
  ```
26
27
 
27
- ## Usage
28
- Using `SafeType` namespace:
28
+ # Use Cases
29
+ ## Environment Variables
29
30
  ```ruby
30
- require 'safe_type'
31
-
32
- SafeType::coerce("SafeType", SafeType::Required::String)
33
- ```
34
-
35
- ### Coercion with Default Value
36
- ```ruby
37
- SafeType::coerce("true", SafeType::Default::Boolean(false)) # => true
38
- SafeType::coerce(nil, SafeType::Default::Boolean(false)) # => false
39
- SafeType::coerce("a", SafeType::Default::Symbol(:a)) # => :a
40
- SafeType::coerce("123", SafeType::Default::Integer(nil)) # => 123
41
- SafeType::coerce("1.0", SafeType::Default::Float(nil)) # => 1.0
42
- SafeType::coerce("2018-06-01", SafeType::Default::Date(nil))
43
- # => #<Date: 2018-06-01 ((2458271j,0s,0n),+0s,2299161j)>
31
+ require 'safe_type/mixin/hash' # symbolize_keys
32
+
33
+ ENV["DISABLE_TASKS"] = "true"
34
+ ENV["API_KEY"] = ""
35
+ ENV["BUILD_NUM"] = "123"
36
+ SAFE_ENV = SafeType::coerce(
37
+ ENV,
38
+ {
39
+ "DISABLE_TASKS" => SafeType::Boolean.default(false),
40
+ "API_KEY" => SafeType::String.default("SECRET"),
41
+ "BUILD_NUM" => SafeType::Integer.strict,
42
+ }
43
+ ).symbolize_keys
44
+
45
+ SAFE_ENV[:DISABLE_TASKS] # => true
46
+ SAFE_ENV[:API_KEY] # => SECRET
47
+ SAFE_ENV[:BUILD_NUM] # => 123
44
48
  ```
45
- ### Coercion with Required Value
49
+ ## Routing Parameters
46
50
  ```ruby
47
- SafeType::coerce("true", SafeType::Required::Boolean) # => true
48
- SafeType::coerce(nil, SafeType::Required::Boolean) # => SafeType::CoercionError
49
- SafeType::coerce("123!", SafeType::Required::Integer) # => SafeType::CoercionError
50
- ```
51
+ class FallSemester < SafeType::Date
52
+ # implement validate method
53
+ end
51
54
 
52
- ### Coercion Rule
53
- Under the hood, all `SafeType::Required` and `SafeType::Default` modules are just
54
- methods for creating coercion rules. A coercion rule has to be described as a hash,
55
- with a required key `type`.
56
- ```ruby
57
- r = Rule.new(type: Integer)
58
- ```
59
- A coercion rules support other parameters such as:
60
- - `required`: If a value is `nil` and has a rule with `required`,
61
- it will raise an exception.
62
- - `default`: If a value is `nil` (not present or failed to convert),
63
- it will be filled with the default.
64
- - `before`: A method will be called before the coercion,
65
- which takes the value to coerce as input.
66
- - `after`: A method will be called after the coercion,
67
- which takes the coercion result as input.
68
- - `validate`: A method will be called to validate the input,
69
- which takes the value to coerce as input. It returns `true` or `false`.
70
- It will empty the value to `nil` if the validation method returns `false`.
71
-
72
- ### Coerce By Rules
73
- Coercion can by defined a set of coercion rules.
74
- If the input is hash-like, then the rules shall be described as the values,
75
- for each key we want to coerce in the input.
76
-
77
- `coerce!` is a mutating method, which modifies the values in place.
78
-
79
- ```ruby
80
- RequiredRecentDate = SafeType::Rule.new(
81
- type: Date, required: true, after: lambda { |date|
82
- date if date >= Date.new(2000, 1, 1) && date <= Date.new(2020, 1, 1)
83
- })
84
-
85
- # => <Date: 2015-01-01 ((2457024j,0s,0n),+0s,2299161j)>
86
- SafeType::coerce("2015-01-01", RequiredRecentDate)
87
- # SafeType::CoercionError
88
- SafeType::coerce("3000-01-01", RequiredRecentDate)
89
- ```
90
- Note mutating coercion can only be applied on a hash-like object.
55
+ current_year = Date.today.year
56
+ params = {
57
+ "course_id" => "101",
58
+ "start_date" => "#{current_year}-10-01"
59
+ }
91
60
 
92
- ```ruby
93
- # ArgumentError: mutating coercion can only be applied on a hash-like object
94
- SafeType::coerce!("1", Rule.new(type: Integer))
95
- ```
61
+ rules = {
62
+ "course_id" => SafeType::Integer.strict,
63
+ "start_date" => FallSemester.strict
64
+ }
96
65
 
97
- ### Coerce Environment Variables
98
- We have to use `String` key and values in `ENV`.
99
- Here is an example of type coercion on `ENV`.
66
+ SafeType::coerce!(params, rules)
100
67
 
101
- ```ruby
102
- ENV["FLAG_0"] = "true"
103
- ENV["FLAG_1"] = "false"
104
- ENV["NUM_0"] = "123"
105
-
106
- h = SafeType::coerce(ENV, {
107
- FLAG_0: SafeType::Default::Boolean(false),
108
- FLAG_1: SafeType::Default::Boolean(false),
109
- NUM_0: SafeType::Default::Integer(0),
110
- }.stringify_keys).symbolize_keys
111
-
112
- h[:FLAG_0] # => true
113
- h[:FLAG_1] # => false
114
- h[:NUM_0] # => 123
68
+ params["course_id"] # => 101
69
+ params["start_date"] # => <Date: 2018-10-01 ((2458393j,0s,0n),+0s,2299161j)>
115
70
  ```
116
-
117
- ### Coerce Hash-like Objects
71
+ ## JSON Response
118
72
  ```ruby
119
- params = {
120
- scores: ["5.0", "3.5", "4.0", "2.2"],
121
- names: ["a", "b", "c", "d"],
73
+ json = {
74
+ "names" => ["Alice", "Bob", "Chris"],
75
+ "info" => [
76
+ {
77
+ "type" => "dog",
78
+ "age" => "5",
79
+ },
80
+ {
81
+ "type" => "cat",
82
+ "age" => "4",
83
+ },
84
+ {
85
+ "type" => "fish",
86
+ "age" => "6",
87
+ }
88
+ ]
122
89
  }
123
90
 
124
- SafeType::coerce!(params, {
125
- scores: [SafeType::Required::Float],
126
- names: [SafeType::Required::String],
91
+ SafeType::coerce!(json, {
92
+ "names" => [SafeType::String.strict],
93
+ "info" => [
94
+ {
95
+ "type" => SafeType::String.strict,
96
+ "age" => SafeType::Integer.strict
97
+ }
98
+ ]
127
99
  })
100
+ ```
101
+ ## Http Response
102
+ ```ruby
103
+ class ResponseType; end
128
104
 
129
- params[:scores] # => [5.0, 3.5, 4.0, 2.2]
130
- params[:names] # => ["a", "b", "c", "d"]
105
+ class Response < SafeType::Rule
106
+ def initialize(type: ResponseType, default: "404")
107
+ super
108
+ end
109
+
110
+ def before(uri)
111
+ # make request
112
+ return ResponseType.new
113
+ end
114
+ end
115
+
116
+ Response["https://API_URI"] # => #<ResponseType:0x000056005b3e7518>
131
117
  ```
132
118
 
133
- ## Prior Art
119
+ # Overview
120
+ A `Rule` describes a single transformation pipeline. It's the core of this gem.
121
+ ```ruby
122
+ class Rule
123
+ def initialize(type:, default: nil, required: false)
124
+ ```
125
+ The parameters are
126
+ - the `type` to transform into
127
+ - the `default` value when the result is `nil`
128
+ - `required` indicates whether empty values are allowed
129
+
130
+ ## `strict` vs `default`
131
+ The primitive types in *SafeType* provide `default` and `strict` mode, which are
132
+ - `SafeType::Boolean`
133
+ - `SafeType::Date`
134
+ - `SafeType::DateTime`
135
+ - `SafeType::Float`
136
+ - `SafeType::Integer`
137
+ - `SafeType::String`
138
+ - `SafeType::Symbol`
139
+ - `SafeType::Time`
140
+
141
+ Under the hood, they are all just SafeType rules.
142
+ - `default`: a rule with default value specified
143
+ - `strict`: a rule with `required: true`, so no empty values are allowed, or it throws `EmptyValueError`
144
+
145
+ ## Apply the rules
146
+ As we've seen in the use cases, we can call `coerce` to apply a set of `SafeType::Rule`s.
147
+ Rules can be bundled together as elements in an array or values in a hash.
148
+
149
+ ### `coerce` vs `coerce!`
150
+ - `SafeType::coerce` returns a new object, corresponding to the rules. The unspecified fields will not be included in the new object.
151
+ - `SafeType::coerce!` coerces the object in place. The unspecified fields will not be modified.
152
+ Note `SafeType::coerce!` cannot be used on a simple object, otherwise it will raise `SafeType::InvalidRuleError`.
153
+
154
+ To apply the rule on a simple object, we can call `[]` method as well.
155
+ ```ruby
156
+ SafeType::Integer.default["1"] # => 1
157
+ SafeType::Integer["1"] # => 1
158
+ ```
159
+ For the *SafeType* primitive types, apply the rule on the class itself will use the default rule.
160
+
161
+ ## Customized Types
162
+ We can inherit from a `SafeType::Rule` to create a customized type.
163
+ We can override following methods if needed:
164
+ - Override `initialize` to change the default values, types, or add more attributes.
165
+ - Override `before` to update the input before convert. This method should take the input and return it after processing.
166
+ - Override `validate` to check the value after convert. This method should take the input and return `true` or `false`.
167
+ - Override `after` to update the input after validate. This method should take the input and return it after processing.
168
+ - Override `handle_exceptions` to change the behavior of exceptions handling (e.g: send to the logger, or no exception)
169
+ - Override `default` or `strict` to modify the default and strict rule.
170
+
171
+ # Prior Art
134
172
  This gem was inspired by [rails_param](https://github.com/nicolasblanco/rails_param)
135
- and [dry-types](https://github.com/dry-rb/dry-types). `dry-types` forces on setting
136
- constrains when creating new object instances. `rails_param` replies on Rails and it is
137
- only for the `params`. Therefore, `safe_type` was created. It integrated some ideas from both
138
- gems, and it was designed specifically for type checking to provide an clean and easy-to-use interface.
173
+ and [dry-types](https://github.com/dry-rb/dry-types). `dry-types` has a complex interface.
174
+ Also it does not support in place coercion, and it will be complicated to `ENV` since the design of its
175
+ `Hash Schemas`. `rails_param` relies on Rails and it is only for the `params`.
176
+ Therefore, `safe_type` was created. It integrated some ideas from both gems,
177
+ and it was designed specifically for type checking to provide an clean and easy-to-use interface.
178
+ It should be useful when working with any string or hash where the values are coming from an external source,
179
+ such as `ENV` variables, rails `params`, or API calls.
139
180
 
140
181
  ## License
141
182
  `safe_type` is released under an MIT license.
@@ -1,249 +1,59 @@
1
- require 'date'
2
- require 'time'
1
+ require 'safe_type/rule'
2
+ require 'safe_type/errors'
3
+
4
+ require 'safe_type/primitive/boolean'
5
+ require 'safe_type/primitive/date'
6
+ require 'safe_type/primitive/date_time'
7
+ require 'safe_type/primitive/float'
8
+ require 'safe_type/primitive/integer'
9
+ require 'safe_type/primitive/string'
10
+ require 'safe_type/primitive/symbol'
11
+ require 'safe_type/primitive/time'
3
12
 
4
13
  module SafeType
5
- module Boolean; end
6
-
7
- module HashHelper
8
- def stringify_keys
9
- Hash[self.map{ |key, val| [key.to_s, val] }]
10
- end
11
- def symbolize_keys
12
- Hash[self.map{ |key, val| [key.to_sym, val] }]
13
- end
14
- end
15
-
16
- class Converter
17
- @@TRUE_VALUES = %w[on On ON t true True TRUE T y yes Yes YES Y].freeze
18
- @@FALSE_VALUES = %w[off Off OFF f false False FALSE F n no No NO N].freeze
19
- @@METHODS = [:to_true, :to_false, :to_int, :to_float, :to_date, :to_date_time,
20
- :to_time].freeze
21
-
22
- def self.to_true(input)
23
- true if @@TRUE_VALUES.include?(input.to_s)
24
- end
25
-
26
- def self.to_false(input)
27
- false if @@FALSE_VALUES.include?(input.to_s)
28
- end
29
-
30
- def self.to_bool(input)
31
- return true unless self.to_true(input).nil?
32
- return false unless self.to_false(input).nil?
33
- end
34
-
35
- def self.to_int(input)
36
- Integer(input, base=10)
37
- end
38
-
39
- def self.to_float(input)
40
- Float(input)
41
- end
42
-
43
- def self.to_date(input)
44
- return input unless input.respond_to?(:to_str)
45
- Date.parse(input)
46
- end
47
-
48
- def self.to_date_time(input)
49
- return input unless input.respond_to?(:to_str)
50
- DateTime.parse(input)
51
- end
52
-
53
- def self.to_time(input)
54
- return input unless input.respond_to?(:to_str)
55
- Time.parse(input)
56
- end
57
-
58
- def self.to_type(input, type)
59
- return input if input.is_a?(type)
60
- return input.to_s if type == String
61
- return input.to_sym if type == Symbol
62
- return self.to_true(input) if type == TrueClass
63
- return self.to_false(input) if type == FalseClass
64
- return self.to_bool(input) if type == Boolean
65
- return self.to_int(input) if type == Integer
66
- return self.to_float(input) if type == Float
67
- return self.to_date(input) if type == Date
68
- return self.to_date_time(input) if type == DateTime
69
- return self.to_time(input) if type == Time
70
- return type.try_convert(input) if type.respond_to?(:try_convert)
71
- return type.new(input) if type.respond_to?(:new)
72
- end
73
- end
74
-
75
- class CoercionError < TypeError
76
- def initialize(msg="unable to transform the input into the requested type.")
77
- super
78
- end
79
- end
80
-
81
- class Rule
82
- attr_reader :type, :default, :before, :after, :validate
83
- def initialize(r)
84
- raise ArgumentError, "SafeType::Rule has to be descried as a hash" \
85
- unless r.class == ::Hash
86
- raise ArgumentError, ":type key is required" \
87
- unless r.has_key?(:type)
88
- raise ArgumentError, ":type has to a class or module" \
89
- unless r[:type].class == ::Class || r[:type].class == ::Module
90
- @type = r[:type]
91
- @required = false
92
- @required = true if r.has_key?(:required) && r[:required]
93
- @has_default = r.has_key?(:default)
94
- @default = r[:default]
95
- @before = r[:before]
96
- @after = r[:after]
97
- @validate = r[:validate]
98
- end
99
-
100
- def required?; @required; end
101
-
102
- def has_default?; @has_default; end
103
-
104
- def apply(input)
105
- input = @before[input] unless @before.nil?
106
- begin
107
- result = Converter.to_type(input, @type) \
108
- if @validate.nil? || (!@validate.nil? && @validate[input])
109
- rescue
110
- return @default if @has_default
111
- return nil unless @required
112
- end
113
- result = @after[result] unless @after.nil?
114
- raise CoercionError if result.nil? && @required
115
- return @default if result.nil?
116
- result
117
- end
118
- end
119
-
120
- module Default
121
- def self.String(val, validate: nil, before: nil, after: nil)
122
- Rule.new(type: String, default: val, validate: validate, before: before, after: after)
123
- end
124
-
125
- def self.Symbol(val, validate: nil, before: nil, after: nil)
126
- Rule.new(type: Symbol, default: val, validate: validate, before: before, after: after)
127
- end
128
-
129
- def self.Boolean(val, validate: nil, before: nil, after: nil)
130
- Rule.new(type: Boolean, default: val, validate: validate, before: before, after: after)
131
- end
132
-
133
- def self.Integer(val, validate: nil, before: nil, after: nil)
134
- Rule.new(type: Integer, default: val, validate: validate, before: before, after: after)
135
- end
136
-
137
- def self.Float(val, validate: nil, before: nil, after: nil)
138
- Rule.new(type: Float, default: val, validate: validate, before: before, after: after)
139
- end
140
-
141
- def self.Date(val, validate: nil, before: nil, after: nil)
142
- Rule.new(type: Date, default: val, validate: validate, before: before, after: after)
143
- end
144
-
145
- def self.DateTime(val, validate: nil, before: nil, after: nil)
146
- Rule.new(type: DateTime, default: val, validate: validate, before: before, after: after)
147
- end
148
-
149
- def self.Time(val, validate: nil, before: nil, after: nil)
150
- Rule.new(type: Time, default: val, validate: validate, before: before, after: after)
151
- end
152
- end
153
-
154
- module Required
155
- def self.String(validate: nil, before: nil, after: nil)
156
- Rule.new(type: ::String, required: true, validate: validate, before: before, after: after)
157
- end
158
-
159
- def self.Symbol(validate: nil, before: nil, after: nil)
160
- Rule.new(type: ::Symbol, required: true, validate: validate, before: before, after: after)
161
- end
162
-
163
- def self.Boolean(validate: nil, before: nil, after: nil)
164
- Rule.new(type: SafeType::Boolean, required: true, validate: validate,
165
- before: before, after: after)
166
- end
167
-
168
- def self.Integer(validate: nil, before: nil, after: nil)
169
- Rule.new(type: ::Integer, required: true, validate: validate, before: before, after: after)
170
- end
171
-
172
- def self.Float(validate: nil, before: nil, after: nil)
173
- Rule.new(type: ::Float, required: true, validate: validate, before: before, after: after)
174
- end
175
-
176
- def self.Date(validate: nil, before: nil, after: nil)
177
- Rule.new(type: ::Date, required: true, validate: validate, before: before, after: after)
178
- end
179
-
180
- def self.DateTime(validate: nil, before: nil, after: nil)
181
- Rule.new(type: ::DateTime, required: true, validate: validate, before: before, after: after)
182
- end
183
-
184
- def self.Time(validate: nil, before: nil, after: nil)
185
- Rule.new(type: ::Time, required: true, validate: validate, before: before, after: after)
186
- end
187
-
188
- String = Rule.new(type: ::String, required: true)
189
- Symbol = Rule.new(type: ::Symbol, required: true)
190
- Boolean = Rule.new(type: SafeType::Boolean, required: true)
191
- Integer = Rule.new(type: ::Integer, required: true)
192
- Float = Rule.new(type: ::Float, required: true)
193
- Date = Rule.new(type: ::Date, required: true)
194
- DateTime = Rule.new(type: ::DateTime, required: true)
195
- Time = Rule.new(type: ::Time, required: true)
196
- end
197
-
198
- def coerce(input, params)
199
- return params.apply(input) if params.class == Rule
200
- if params.class == ::Hash
201
- result = {}
202
- params.each do |key, val|
203
- result[key] = coerce(input[key], val)
14
+ class << self
15
+ def coerce(input, rule)
16
+ return rule[input] if rule.is_a?(SafeType::Rule)
17
+ if rule.class == ::Hash
18
+ result = {}
19
+ rule.each do |key, val|
20
+ result[key] = coerce(input[key], val)
21
+ end
22
+ return result
204
23
  end
205
- return result
206
- end
207
- if params.class == ::Array
208
- return [] if input.nil?
209
- result = Array.new(input.length)
210
- i = 0
211
- while i < input.length
212
- result[i] = coerce(input[i], params[i % params.length])
213
- i += 1
24
+ if rule.class == ::Array
25
+ return [] if input.nil?
26
+ result = ::Array.new(input.length)
27
+ i = 0
28
+ while i < input.length
29
+ result[i] = coerce(input[i], rule[i % rule.length])
30
+ i += 1
31
+ end
32
+ return result
214
33
  end
215
- return result
34
+ raise SafeType::InvalidRuleError
216
35
  end
217
- raise ArgumentError, "invalid coercion rule"
218
- end
219
36
 
220
- def coerce!(input, params)
221
- if params.class == ::Hash
222
- params.each do |key, val|
223
- if val.class == ::Hash
224
- coerce!(input[key], val)
225
- else
226
- input[key] = coerce(input[key], val)
37
+ def coerce!(input, rule)
38
+ if rule.class == ::Hash
39
+ rule.each do |key, val|
40
+ if val.class == ::Hash
41
+ coerce!(input[key], val)
42
+ else
43
+ input[key] = coerce(input[key], val)
44
+ end
227
45
  end
46
+ return nil
228
47
  end
229
- return nil
230
- end
231
- if params.class == ::Array
232
- i = 0
233
- while i < input.length
234
- input[i] = coerce(input[i], params[i % params.length])
235
- i += 1
48
+ if rule.class == ::Array
49
+ i = 0
50
+ while i < input.length
51
+ input[i] = coerce(input[i], rule[i % rule.length])
52
+ i += 1
53
+ end
54
+ return nil
236
55
  end
237
- return nil
56
+ raise SafeType::InvalidRuleError
238
57
  end
239
- raise ArgumentError, "invalid coercion rule"
240
- end
241
-
242
- class << self
243
- include SafeType
244
58
  end
245
59
  end
246
-
247
- class TrueClass; include SafeType::Boolean; end
248
- class FalseClass; include SafeType::Boolean; end
249
- class Hash; include SafeType::HashHelper; end
@@ -0,0 +1,64 @@
1
+ require 'date'
2
+ require 'time'
3
+
4
+ module SafeType
5
+ class Converter
6
+ @@TRUE_VALUES = %w[on On ON t true True TRUE T y yes Yes YES Y].freeze
7
+ @@FALSE_VALUES = %w[off Off OFF f false False FALSE F n no No NO N].freeze
8
+
9
+ def self.to_true(input)
10
+ true if @@TRUE_VALUES.include?(input.to_s)
11
+ end
12
+
13
+ def self.to_false(input)
14
+ false if @@FALSE_VALUES.include?(input.to_s)
15
+ end
16
+
17
+ def self.to_bool(input)
18
+ return true unless self.to_true(input).nil?
19
+ return false unless self.to_false(input).nil?
20
+ raise TypeError
21
+ end
22
+
23
+ def self.to_int(input)
24
+ if input.is_a?(::String)
25
+ Integer(input, base=10)
26
+ else
27
+ Integer(input)
28
+ end
29
+ end
30
+
31
+ def self.to_float(input)
32
+ Float(input)
33
+ end
34
+
35
+ def self.to_date(input)
36
+ ::Date.parse(input)
37
+ end
38
+
39
+ def self.to_date_time(input)
40
+ ::DateTime.parse(input)
41
+ end
42
+
43
+ def self.to_time(input)
44
+ ::Time.parse(input)
45
+ end
46
+
47
+ def self.to_type(input, type)
48
+ return input if input.is_a?(type)
49
+ return input.safe_type if input.respond_to?(:safe_type)
50
+ return input.to_s if type == ::String
51
+ return input.to_sym if type == ::Symbol
52
+ return self.to_true(input) if type == ::TrueClass
53
+ return self.to_false(input) if type == ::FalseClass
54
+ return self.to_bool(input) if type == SafeType::BooleanMixin
55
+ return self.to_int(input) if type == ::Integer
56
+ return self.to_float(input) if type == ::Float
57
+ return self.to_date(input) if type == ::Date
58
+ return self.to_date_time(input) if type == ::DateTime
59
+ return self.to_time(input) if type == ::Time
60
+ return type.try_convert(input) if type.respond_to?(:try_convert)
61
+ return type.new(input) if type.respond_to?(:new)
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,27 @@
1
+ module SafeType
2
+ class Error < StandardError; end
3
+
4
+ class CoercionError < Error
5
+ def initialize(message="unable to transform into the requested type")
6
+ super
7
+ end
8
+ end
9
+
10
+ class ValidationError < Error
11
+ def initialize(message="failed to validate")
12
+ super
13
+ end
14
+ end
15
+
16
+ class EmptyValueError < Error
17
+ def initialize(message="the value should not be empty")
18
+ super
19
+ end
20
+ end
21
+
22
+ class InvalidRuleError < ArgumentError
23
+ def initialize(message="invalid coercion rule")
24
+ super
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,12 @@
1
+ module SafeType
2
+ module BooleanMixin
3
+ end
4
+ end
5
+
6
+ class TrueClass
7
+ include SafeType::BooleanMixin
8
+ end
9
+
10
+ class FalseClass
11
+ include SafeType::BooleanMixin
12
+ end
@@ -0,0 +1,14 @@
1
+ module SafeType
2
+ module HashMixin
3
+ def stringify_keys
4
+ ::Hash[self.map{ |key, val| [key.to_s, val] }]
5
+ end
6
+ def symbolize_keys
7
+ ::Hash[self.map{ |key, val| [key.to_sym, val] }]
8
+ end
9
+ end
10
+ end
11
+
12
+ class Hash
13
+ include SafeType::HashMixin
14
+ end
@@ -0,0 +1,13 @@
1
+ require 'safe_type/mixin/boolean'
2
+
3
+ module SafeType
4
+ class Boolean < Rule
5
+ def initialize(type: ::SafeType::BooleanMixin, **args)
6
+ super
7
+ end
8
+
9
+ def self.default(value=false)
10
+ new(default: value)
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,31 @@
1
+ module SafeType
2
+ class Date < Rule
3
+ def initialize(type: ::Date, from: nil, to: nil, **args)
4
+ @from = from
5
+ @to = to
6
+ super
7
+ end
8
+
9
+ def validate(input)
10
+ return false unless @from.nil? || input >= @from
11
+ return false unless @to.nil? || input <= @to
12
+ super
13
+ end
14
+
15
+ def self.default(value=nil, from: nil, to: nil)
16
+ new(
17
+ default: value,
18
+ from: from,
19
+ to: to
20
+ )
21
+ end
22
+
23
+ def self.strict(from: nil, to: nil)
24
+ new(
25
+ required: true,
26
+ from: from,
27
+ to: to
28
+ )
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,31 @@
1
+ module SafeType
2
+ class DateTime < Rule
3
+ def initialize(type: ::DateTime, from: nil, to: nil, **args)
4
+ @from = from
5
+ @to = to
6
+ super
7
+ end
8
+
9
+ def validate(input)
10
+ return false unless @from.nil? || input >= @from
11
+ return false unless @to.nil? || input <= @to
12
+ super
13
+ end
14
+
15
+ def self.default(value=nil, from: nil, to: nil)
16
+ new(
17
+ default: value,
18
+ from: from,
19
+ to: to
20
+ )
21
+ end
22
+
23
+ def self.strict(from: nil, to: nil)
24
+ new(
25
+ required: true,
26
+ from: from,
27
+ to: to
28
+ )
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,31 @@
1
+ module SafeType
2
+ class Float < Rule
3
+ def initialize(type: ::Float, min: nil, max: nil, **args)
4
+ @min = min
5
+ @max = max
6
+ super
7
+ end
8
+
9
+ def validate(input)
10
+ return false unless @min.nil? || input >= @min
11
+ return false unless @max.nil? || input <= @max
12
+ super
13
+ end
14
+
15
+ def self.default(value=0.0, min: nil, max: nil)
16
+ new(
17
+ default: value,
18
+ min: min,
19
+ max: max
20
+ )
21
+ end
22
+
23
+ def self.strict(min: nil, max: nil)
24
+ new(
25
+ required: true,
26
+ min: min,
27
+ max: max
28
+ )
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,31 @@
1
+ module SafeType
2
+ class Integer < Rule
3
+ def initialize(type: ::Integer, min: nil, max: nil, **args)
4
+ @min = min
5
+ @max = max
6
+ super
7
+ end
8
+
9
+ def validate(input)
10
+ return false unless @min.nil? || input >= @min
11
+ return false unless @max.nil? || input <= @max
12
+ super
13
+ end
14
+
15
+ def self.default(value=0, min: nil, max: nil)
16
+ new(
17
+ default: value,
18
+ min: min,
19
+ max: max
20
+ )
21
+ end
22
+
23
+ def self.strict(min: nil, max: nil)
24
+ new(
25
+ required: true,
26
+ min: min,
27
+ max: max
28
+ )
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,36 @@
1
+ module SafeType
2
+ class String < Rule
3
+ def initialize(type: ::String, min_length: nil, max_length: nil, **args)
4
+ @min_length = min_length
5
+ @max_length = max_length
6
+ super
7
+ end
8
+
9
+ def validate(input)
10
+ return false unless @min_length.nil? || input.length >= @min_length
11
+ return false unless @max_length.nil? || input.length <= @max_length
12
+ super
13
+ end
14
+
15
+ def after(input)
16
+ return nil if input.length == 0
17
+ super
18
+ end
19
+
20
+ def self.default(value="", min_length: nil, max_length: nil)
21
+ new(
22
+ default: value,
23
+ min_length: min_length,
24
+ max_length: max_length
25
+ )
26
+ end
27
+
28
+ def self.strict(min_length: nil, max_length: nil)
29
+ new(
30
+ required: true,
31
+ min_length: min_length,
32
+ max_length: max_length
33
+ )
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,36 @@
1
+ module SafeType
2
+ class Symbol < Rule
3
+ def initialize(type: ::Symbol, min_length: nil, max_length: nil, **args)
4
+ @min_length = min_length
5
+ @max_length = max_length
6
+ super
7
+ end
8
+
9
+ def validate(input)
10
+ return false unless @min_length.nil? || input.length >= @min_length
11
+ return false unless @max_length.nil? || input.length <= @max_length
12
+ super
13
+ end
14
+
15
+ def after(input)
16
+ return nil if input.length == 0
17
+ super
18
+ end
19
+
20
+ def self.default(value=nil, min_length: nil, max_length: nil)
21
+ new(
22
+ default: value,
23
+ min_length: min_length,
24
+ max_length: max_length
25
+ )
26
+ end
27
+
28
+ def self.strict(min_length: nil, max_length: nil)
29
+ new(
30
+ required: true,
31
+ min_length: min_length,
32
+ max_length: max_length
33
+ )
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,31 @@
1
+ module SafeType
2
+ class Time < Rule
3
+ def initialize(type: ::Time, from: nil, to: nil, **args)
4
+ @from = from
5
+ @to = to
6
+ super
7
+ end
8
+
9
+ def validate(input)
10
+ return false unless @from.nil? || input >= @from
11
+ return false unless @to.nil? || input <= @to
12
+ super
13
+ end
14
+
15
+ def self.default(value=nil, from: nil, to: nil)
16
+ new(
17
+ default: value,
18
+ from: from,
19
+ to: to
20
+ )
21
+ end
22
+
23
+ def self.strict(from: nil, to: nil)
24
+ new(
25
+ required: true,
26
+ from: from,
27
+ to: to
28
+ )
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,58 @@
1
+ require 'safe_type/converter'
2
+ require 'safe_type/errors'
3
+
4
+ module SafeType
5
+ class Rule
6
+ def initialize(type:, default: nil, required: false, **args)
7
+ unless type.class == ::Class || type.class == ::Module
8
+ raise ArgumentError.new("type has to a class or module")
9
+ end
10
+ @type = type
11
+ @required = required
12
+ @default = default
13
+ end
14
+
15
+ def validate(input)
16
+ true
17
+ end
18
+
19
+ def before(input)
20
+ input
21
+ end
22
+
23
+ def after(input)
24
+ input
25
+ end
26
+
27
+ def handle_exceptions(e)
28
+ raise SafeType::CoercionError
29
+ end
30
+
31
+ def self.[](input)
32
+ default[input]
33
+ end
34
+
35
+ def self.default
36
+ new
37
+ end
38
+
39
+ def self.strict
40
+ new(required: true)
41
+ end
42
+
43
+ def [](input)
44
+ raise SafeType::EmptyValueError if input.nil? && @required
45
+ input = before(input)
46
+ input = Converter.to_type(input, @type)
47
+ raise SafeType::ValidationError unless validate(input)
48
+ result = after(input)
49
+ raise SafeType::EmptyValueError if result.nil? && @required
50
+ return @default if result.nil?
51
+ raise SafeType::CoercionError unless result.is_a?(@type)
52
+ result
53
+ rescue TypeError, ArgumentError, NoMethodError => e
54
+ return @default if input.nil? && !@required
55
+ handle_exceptions(e)
56
+ end
57
+ end
58
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: safe_type
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.3
4
+ version: 0.0.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Donald Dong
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-06-08 00:00:00.000000000 Z
11
+ date: 2018-06-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake
@@ -46,6 +46,19 @@ extra_rdoc_files: []
46
46
  files:
47
47
  - README.md
48
48
  - lib/safe_type.rb
49
+ - lib/safe_type/converter.rb
50
+ - lib/safe_type/errors.rb
51
+ - lib/safe_type/mixin/boolean.rb
52
+ - lib/safe_type/mixin/hash.rb
53
+ - lib/safe_type/primitive/boolean.rb
54
+ - lib/safe_type/primitive/date.rb
55
+ - lib/safe_type/primitive/date_time.rb
56
+ - lib/safe_type/primitive/float.rb
57
+ - lib/safe_type/primitive/integer.rb
58
+ - lib/safe_type/primitive/string.rb
59
+ - lib/safe_type/primitive/symbol.rb
60
+ - lib/safe_type/primitive/time.rb
61
+ - lib/safe_type/rule.rb
49
62
  homepage: https://github.com/chanzuckerberg/safe_type
50
63
  licenses:
51
64
  - MIT