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 +4 -4
- data/README.md +141 -100
- data/lib/safe_type.rb +47 -237
- data/lib/safe_type/converter.rb +64 -0
- data/lib/safe_type/errors.rb +27 -0
- data/lib/safe_type/mixin/boolean.rb +12 -0
- data/lib/safe_type/mixin/hash.rb +14 -0
- data/lib/safe_type/primitive/boolean.rb +13 -0
- data/lib/safe_type/primitive/date.rb +31 -0
- data/lib/safe_type/primitive/date_time.rb +31 -0
- data/lib/safe_type/primitive/float.rb +31 -0
- data/lib/safe_type/primitive/integer.rb +31 -0
- data/lib/safe_type/primitive/string.rb +36 -0
- data/lib/safe_type/primitive/symbol.rb +36 -0
- data/lib/safe_type/primitive/time.rb +31 -0
- data/lib/safe_type/rule.rb +58 -0
- metadata +15 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a67c63237e5cf69ced4be0ee3a485f7ff3fba588
|
4
|
+
data.tar.gz: f92d5b559d4a2cbf322187f2f4eef1d17d7967a5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6fab8fcf295c05a748678d0ed5f4770797f8b83c3d637fe5e70eedb0fd62f405f8703577a4dd2cfb7f45efb515437c8bc7064f6f993c945a2dc80c5d84db519d
|
7
|
+
data.tar.gz: 311fe81e529e0f948cbefadf8d2efea6cd992897a692468c253579f50ad2606f663c890bad05d22037da850a55d76abf4dcd46ef6a879fc503b2bd527ea3f756
|
data/README.md
CHANGED
@@ -1,16 +1,17 @@
|
|
1
|
-
|
1
|
+
SafeType
|
2
|
+
======
|
2
3
|
[](https://badge.fury.io/rb/safe_type)
|
3
4
|
[](https://travis-ci.org/chanzuckerberg/safe_type)
|
4
5
|
[](https://codeclimate.com/github/chanzuckerberg/safe_type/maintainability)
|
5
6
|
[](https://codeclimate.com/github/chanzuckerberg/safe_type/test_coverage)
|
6
7
|
|
7
|
-
While working with environment variables, routing parameters,
|
8
|
+
While working with environment variables, routing parameters, API responses,
|
8
9
|
or other Hash-like objects require parsing,
|
9
|
-
we often
|
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
|
-
|
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
|
-
|
28
|
-
|
28
|
+
# Use Cases
|
29
|
+
## Environment Variables
|
29
30
|
```ruby
|
30
|
-
require 'safe_type'
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
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
|
-
|
49
|
+
## Routing Parameters
|
46
50
|
```ruby
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
```
|
51
|
+
class FallSemester < SafeType::Date
|
52
|
+
# implement validate method
|
53
|
+
end
|
51
54
|
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
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
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
61
|
+
rules = {
|
62
|
+
"course_id" => SafeType::Integer.strict,
|
63
|
+
"start_date" => FallSemester.strict
|
64
|
+
}
|
96
65
|
|
97
|
-
|
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
|
-
|
102
|
-
|
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
|
-
|
120
|
-
|
121
|
-
|
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!(
|
125
|
-
|
126
|
-
|
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
|
-
|
130
|
-
|
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
|
-
|
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`
|
136
|
-
|
137
|
-
|
138
|
-
|
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.
|
data/lib/safe_type.rb
CHANGED
@@ -1,249 +1,59 @@
|
|
1
|
-
require '
|
2
|
-
require '
|
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
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
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
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
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
|
-
|
34
|
+
raise SafeType::InvalidRuleError
|
216
35
|
end
|
217
|
-
raise ArgumentError, "invalid coercion rule"
|
218
|
-
end
|
219
36
|
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
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
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
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
|
-
|
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,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,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.
|
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-
|
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
|