safe_type 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (4) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +141 -0
  3. data/lib/safe_type.rb +249 -0
  4. 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
+ [![Gem Version](https://badge.fury.io/rb/safe_type.svg)](https://badge.fury.io/rb/safe_type)
3
+ [![Build Status](https://travis-ci.org/chanzuckerberg/safe_type.svg?branch=master)](https://travis-ci.org/chanzuckerberg/safe_type)
4
+ [![Maintainability](https://api.codeclimate.com/v1/badges/7fbc9a4038b86ef639e1/maintainability)](https://codeclimate.com/github/chanzuckerberg/safe_type/maintainability)
5
+ [![Test Coverage](https://api.codeclimate.com/v1/badges/7fbc9a4038b86ef639e1/test_coverage)](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: []