safe_type 0.0.3 → 0.0.4

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