safe_type 0.0.3
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 +7 -0
- data/README.md +141 -0
- data/lib/safe_type.rb +249 -0
- metadata +74 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: eca25594ac5b8d10503472a6adc49dba43091c3f
|
4
|
+
data.tar.gz: c469b2f8539bc78995b69b200757c0c420997b75
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 8f56662fd4b888c637db732c23d9248d14691af30d95499fd73e511d1cff836c5816f22b816960c9c0bcbb65aecc08f7a11f037cb9023a762d58b5bb925b3e3d
|
7
|
+
data.tar.gz: 4b3bc19ab41437031109d8922a9187623a5912e0dd295ab3461e26c80a44e98b5f031d0a4c15e14e1da7f552d401ab60589c14bac1cc0c5a692493b5fa3f76e9
|
data/README.md
ADDED
@@ -0,0 +1,141 @@
|
|
1
|
+
# SafeType
|
2
|
+
[](https://badge.fury.io/rb/safe_type)
|
3
|
+
[](https://travis-ci.org/chanzuckerberg/safe_type)
|
4
|
+
[](https://codeclimate.com/github/chanzuckerberg/safe_type/maintainability)
|
5
|
+
[](https://codeclimate.com/github/chanzuckerberg/safe_type/test_coverage)
|
6
|
+
|
7
|
+
While working with environment variables, routing parameters, JSON objects,
|
8
|
+
or other Hash-like objects require parsing,
|
9
|
+
we often require type coercion to assure expected behaviors.
|
10
|
+
|
11
|
+
***SafeType*** provides an intuitive type coercion interface and type enhancement.
|
12
|
+
|
13
|
+
## Install
|
14
|
+
|
15
|
+
We can install `safe_type` using `gem install`:
|
16
|
+
|
17
|
+
```bash
|
18
|
+
gem install safe_type
|
19
|
+
```
|
20
|
+
|
21
|
+
Or we can add it as a dependency in the `Gemfile` and run `bundle install`:
|
22
|
+
|
23
|
+
```ruby
|
24
|
+
gem 'safe_type'
|
25
|
+
```
|
26
|
+
|
27
|
+
## Usage
|
28
|
+
Using `SafeType` namespace:
|
29
|
+
```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)>
|
44
|
+
```
|
45
|
+
### Coercion with Required Value
|
46
|
+
```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
|
+
|
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.
|
91
|
+
|
92
|
+
```ruby
|
93
|
+
# ArgumentError: mutating coercion can only be applied on a hash-like object
|
94
|
+
SafeType::coerce!("1", Rule.new(type: Integer))
|
95
|
+
```
|
96
|
+
|
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`.
|
100
|
+
|
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
|
115
|
+
```
|
116
|
+
|
117
|
+
### Coerce Hash-like Objects
|
118
|
+
```ruby
|
119
|
+
params = {
|
120
|
+
scores: ["5.0", "3.5", "4.0", "2.2"],
|
121
|
+
names: ["a", "b", "c", "d"],
|
122
|
+
}
|
123
|
+
|
124
|
+
SafeType::coerce!(params, {
|
125
|
+
scores: [SafeType::Required::Float],
|
126
|
+
names: [SafeType::Required::String],
|
127
|
+
})
|
128
|
+
|
129
|
+
params[:scores] # => [5.0, 3.5, 4.0, 2.2]
|
130
|
+
params[:names] # => ["a", "b", "c", "d"]
|
131
|
+
```
|
132
|
+
|
133
|
+
## Prior Art
|
134
|
+
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.
|
139
|
+
|
140
|
+
## License
|
141
|
+
`safe_type` is released under an MIT license.
|
data/lib/safe_type.rb
ADDED
@@ -0,0 +1,249 @@
|
|
1
|
+
require 'date'
|
2
|
+
require 'time'
|
3
|
+
|
4
|
+
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)
|
204
|
+
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
|
214
|
+
end
|
215
|
+
return result
|
216
|
+
end
|
217
|
+
raise ArgumentError, "invalid coercion rule"
|
218
|
+
end
|
219
|
+
|
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)
|
227
|
+
end
|
228
|
+
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
|
236
|
+
end
|
237
|
+
return nil
|
238
|
+
end
|
239
|
+
raise ArgumentError, "invalid coercion rule"
|
240
|
+
end
|
241
|
+
|
242
|
+
class << self
|
243
|
+
include SafeType
|
244
|
+
end
|
245
|
+
end
|
246
|
+
|
247
|
+
class TrueClass; include SafeType::Boolean; end
|
248
|
+
class FalseClass; include SafeType::Boolean; end
|
249
|
+
class Hash; include SafeType::HashHelper; end
|
metadata
ADDED
@@ -0,0 +1,74 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: safe_type
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.3
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Donald Dong
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2018-06-08 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rake
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rspec
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
description: " \n Type coercion & Type Enhancement\n "
|
42
|
+
email: mail@ddong.me
|
43
|
+
executables: []
|
44
|
+
extensions: []
|
45
|
+
extra_rdoc_files: []
|
46
|
+
files:
|
47
|
+
- README.md
|
48
|
+
- lib/safe_type.rb
|
49
|
+
homepage: https://github.com/chanzuckerberg/safe_type
|
50
|
+
licenses:
|
51
|
+
- MIT
|
52
|
+
metadata: {}
|
53
|
+
post_install_message:
|
54
|
+
rdoc_options:
|
55
|
+
- "--charset=UTF-8"
|
56
|
+
require_paths:
|
57
|
+
- lib
|
58
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
59
|
+
requirements:
|
60
|
+
- - ">="
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: '0'
|
63
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - ">="
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '0'
|
68
|
+
requirements: []
|
69
|
+
rubyforge_project:
|
70
|
+
rubygems_version: 2.5.2.1
|
71
|
+
signing_key:
|
72
|
+
specification_version: 4
|
73
|
+
summary: Type coercion & Type Enhancement
|
74
|
+
test_files: []
|