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.
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: []