constrain 0.1.3 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +134 -69
- data/TODO +1 -0
- data/constrain.gemspec +1 -1
- data/lib/constrain/version.rb +1 -1
- data/lib/constrain.rb +65 -15
- metadata +5 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: fda0b95a5d43c197c9df1134d68dbe4fbc1f8f71ad9dc7a8e995daf729eaea2c
|
4
|
+
data.tar.gz: 2fe722e5085bc9b396e203b3f3514a1f123c18843e293f902392519ef6a3d5ed
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: bc98410262f21b5fea23182cad7c9e80e48a4aff7ff64dc7ff2113581440a191bfa9b77a28045624caf4f6e3102251e175841604c64461287ea7eb1062174bf2
|
7
|
+
data.tar.gz: c19a11b5ab9e22b49ad2204b276658d26b109296815e06298620fdb53104f6ce70625095b4ac5b3fbd3aff79664866de7afb3507cea1b5d1737aaf714c902c7e
|
data/README.md
CHANGED
@@ -2,9 +2,10 @@
|
|
2
2
|
|
3
3
|
`Constrain` allows you to check if an object match a class expression. It is
|
4
4
|
typically used to check the type of method parameters and is an alternative to
|
5
|
-
using Ruby-3 .rbs files
|
5
|
+
using Ruby-3 .rbs files
|
6
6
|
|
7
7
|
```ruby
|
8
|
+
require 'constrain'
|
8
9
|
include Constrain
|
9
10
|
|
10
11
|
# f takes a String and an array of Integer objects and raises otherwise
|
@@ -23,22 +24,6 @@ production (TODO: Make it possible to deactivate)
|
|
23
24
|
|
24
25
|
Constrain works with ruby-2 (and maybe ruby-3)
|
25
26
|
|
26
|
-
## Installation
|
27
|
-
|
28
|
-
Add this line to your application's Gemfile:
|
29
|
-
|
30
|
-
```ruby
|
31
|
-
gem 'constrain'
|
32
|
-
```
|
33
|
-
|
34
|
-
And then execute:
|
35
|
-
|
36
|
-
$ bundle install
|
37
|
-
|
38
|
-
Or install it yourself as:
|
39
|
-
|
40
|
-
$ gem install constrain
|
41
|
-
|
42
27
|
## Usage
|
43
28
|
|
44
29
|
You will typically include Constrain globally to have #constrain available everywhere
|
@@ -58,88 +43,95 @@ end
|
|
58
43
|
```
|
59
44
|
|
60
45
|
The alternative is to include the constrain Module in a common root class to
|
61
|
-
have it available in all child
|
46
|
+
have it available in all child classes
|
62
47
|
|
63
|
-
|
48
|
+
## Methods
|
64
49
|
|
65
|
-
|
66
|
-
constrain(value, *class-expressions, message = nil)
|
67
|
-
```
|
50
|
+
#### constrain(value, \*expressions, message = nil, unwind: 0)
|
68
51
|
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
52
|
+
Return the given value if it matches at least one of the expressions and raise a
|
53
|
+
Constrain::TypeError if not. The value is matched against the expressions using
|
54
|
+
the #=== operator so anything you can put into the 'when' clause of a 'case'
|
55
|
+
statement can be used. #constrain raise a Constrain::MatchError if the value
|
56
|
+
doesn't match any expression
|
74
57
|
|
75
|
-
|
58
|
+
The error message can be customized by added the message argument and a number
|
59
|
+
of backtrace leves can be skipped by setting :unwind option. By default the
|
60
|
+
backtrace will refer to the point of the call of \#constrain. \#constrain
|
61
|
+
raises a Constrain::Error exception if there is an error in the syntax of the
|
62
|
+
class expression
|
76
63
|
|
77
|
-
|
78
|
-
|
79
|
-
|
64
|
+
\#constrain is typically used to type-check parameters in methods where you
|
65
|
+
want an exception if the parameters doesn't match the expected, but because it
|
66
|
+
returns the value if successful it can be used to check the validity of
|
67
|
+
variables in expressions too, eg. `return constrain(result_of_complex_computation, Integer)`
|
68
|
+
to check the return value of a method
|
80
69
|
|
81
|
-
|
82
|
-
or false as result
|
70
|
+
#### Constrain.constrain(value, \*expressions, message = nil, unwind: 0)
|
83
71
|
|
84
|
-
|
72
|
+
Class method version of #constrain. It is automatically added to classes that
|
73
|
+
include Constrain
|
85
74
|
|
86
|
-
Constrain#constrain and Constrain::check use class expressions composed of
|
87
|
-
class or module objects, Proc objects, or arrays and hashes of class expressions. Class or module
|
88
|
-
objects match if `value.is_a?(class_or_module)` returns true:
|
89
75
|
|
90
|
-
|
91
|
-
constrain 42, Integer # Success
|
92
|
-
constrain 42, Comparable # Success
|
93
|
-
constrain nil, Comparable # Failure
|
94
|
-
```
|
76
|
+
#### Constrain.constrain?(value, \*expressions) -> true or false
|
95
77
|
|
96
|
-
|
78
|
+
It matches value against the class expressions like #constrain but returns true
|
79
|
+
or false as result. It is automatically added to classes that include
|
80
|
+
Constrain. Constrain.constrain? raises a Constrain::Error exception if there
|
81
|
+
is an error in the syntax of the class expression
|
97
82
|
|
98
|
-
```ruby
|
99
|
-
constrain "str", Symbol, String # Success
|
100
|
-
constrain :sym, Symbol, String # Success
|
101
|
-
constrain 42, Symbol, String # Failure
|
102
|
-
```
|
103
83
|
|
104
|
-
|
84
|
+
## Expressions
|
85
|
+
|
86
|
+
Expressions can be simple values, class expressions, or lambdas. You can mix
|
87
|
+
simple values and class expressions but not lambdas
|
105
88
|
|
106
|
-
|
89
|
+
|
90
|
+
### Simple expressions
|
91
|
+
|
92
|
+
Simple values is an easy way to check arguments with a limited set of allowed
|
93
|
+
values like
|
107
94
|
|
108
95
|
```ruby
|
109
|
-
|
110
|
-
constrain
|
96
|
+
def print_color(color)
|
97
|
+
constrain color, :red, :yellow, :green
|
98
|
+
...
|
99
|
+
end
|
111
100
|
```
|
112
101
|
|
113
|
-
|
114
|
-
|
102
|
+
Simple values are compared to the expected result using the #=== operator. This
|
103
|
+
means you can use regular expressions too:
|
115
104
|
|
116
105
|
```ruby
|
117
|
-
|
118
|
-
|
119
|
-
|
106
|
+
# Simple email regular expression (https://stackoverflow.com/a/719543)
|
107
|
+
EMAIL_RE = /^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$/
|
108
|
+
|
109
|
+
def email(address)
|
110
|
+
constrain address, EMAIL_RE
|
111
|
+
...
|
112
|
+
end
|
120
113
|
```
|
121
114
|
|
122
|
-
|
115
|
+
### Class Expressions
|
123
116
|
|
124
|
-
|
117
|
+
Constrain#constrain and Constrain::constrain? use class expressions composed of
|
118
|
+
class or module objects, Proc objects, or arrays and hashes of class expressions. Class or module
|
119
|
+
objects match if `value.is_a?(class_or_module)` returns true:
|
125
120
|
|
126
121
|
```ruby
|
127
|
-
constrain 42,
|
128
|
-
constrain
|
122
|
+
constrain 42, Integer # Success
|
123
|
+
constrain 42, Comparable # Success
|
124
|
+
constrain nil, Comparable # Failure
|
129
125
|
```
|
130
126
|
|
131
|
-
|
127
|
+
More than one class expression is allowed. It matches if at least one of the expressions match:
|
132
128
|
|
133
129
|
```ruby
|
134
|
-
constrain
|
135
|
-
constrain
|
130
|
+
constrain "str", Symbol, String # Success
|
131
|
+
constrain :sym, Symbol, String # Success
|
132
|
+
constrain 42, Symbol, String # Failure
|
136
133
|
```
|
137
134
|
|
138
|
-
Proc objects can check every aspect of an object but you should not overuse it
|
139
|
-
because as checks becomes more complex they tend to include business logic that
|
140
|
-
should be kept in the production code. Constrain is only thouhgt of as a tool
|
141
|
-
to catch developer errors - not errors that stem from corrupted data
|
142
|
-
|
143
135
|
#### Arrays
|
144
136
|
|
145
137
|
Arrays match if the value is an Array and all its element match the given class expression:
|
@@ -196,6 +188,79 @@ sure the list expression is enclosed in an array:
|
|
196
188
|
constrain({ [sym] => 42 }, [[Symbol, String]] => Integer) # Success
|
197
189
|
```
|
198
190
|
|
191
|
+
#### nil, true and false
|
192
|
+
|
193
|
+
NilClass is a valid argument and can be used to allow nil values:
|
194
|
+
|
195
|
+
```ruby
|
196
|
+
constrain nil, Integer # Failure
|
197
|
+
constrain nil, Integer, NilClass # Success
|
198
|
+
```
|
199
|
+
|
200
|
+
Boolean values are a special case since ruby doesn't have a boolean type use a
|
201
|
+
list to match for a boolean argument:
|
202
|
+
|
203
|
+
```ruby
|
204
|
+
constrain true, TrueClass, FalseClass # Success
|
205
|
+
constrain false, TrueClass, FalseClass # Success
|
206
|
+
constrain nil, TrueClass, FalseClass # Failure
|
207
|
+
```
|
208
|
+
|
209
|
+
But note that it is often easier to use value expressions:
|
210
|
+
|
211
|
+
```ruby
|
212
|
+
constrain true, true, false # Success
|
213
|
+
constrain false, true, false # Success
|
214
|
+
constrain nil, true, false # Failure
|
215
|
+
constrain nil, true, false, nil # Success
|
216
|
+
```
|
217
|
+
|
218
|
+
### Lambda expressions
|
219
|
+
|
220
|
+
Proc objects are called with the value as argument and should return truish or falsy:
|
221
|
+
|
222
|
+
```ruby
|
223
|
+
constrain 42, lambda { |value| value > 1 } # Success
|
224
|
+
constrain 0, lambda { |value| value > 1 } # Failure
|
225
|
+
```
|
226
|
+
|
227
|
+
Note that it is not possible to first match against a class expression and then
|
228
|
+
use the proc object. You will either have to check for the type too in the proc
|
229
|
+
object or make two calls to #constrain:
|
230
|
+
|
231
|
+
```ruby
|
232
|
+
constrain 0, Integer # Success
|
233
|
+
constrain 0, lambda { |value| value > 1 } # Failure
|
234
|
+
```
|
235
|
+
|
236
|
+
Alternatively, you can use Constrain::constrain? to mix classes or value with lambdas:
|
237
|
+
|
238
|
+
```ruby
|
239
|
+
constrain 0, lambda { |value| Constrain::constrain?(Integer) && value > 1 } # Failure
|
240
|
+
```
|
241
|
+
|
242
|
+
Note that even though Proc objects can check every aspect of an object, you
|
243
|
+
should not overuse it because as checks becomes more complex they tend to
|
244
|
+
include business logic that should be kept in the production code. Constrain is
|
245
|
+
only thouhgt of as a tool to catch developer errors - not errors that stem from
|
246
|
+
corrupted data
|
247
|
+
|
248
|
+
## Installation
|
249
|
+
|
250
|
+
Add this line to your application's Gemfile:
|
251
|
+
|
252
|
+
```ruby
|
253
|
+
gem 'constrain'
|
254
|
+
```
|
255
|
+
|
256
|
+
And then execute:
|
257
|
+
|
258
|
+
$ bundle install
|
259
|
+
|
260
|
+
Or install it yourself as:
|
261
|
+
|
262
|
+
$ gem install constrain
|
263
|
+
|
199
264
|
## Contributing
|
200
265
|
|
201
266
|
Bug reports and pull requests are welcome on GitHub at https://github.com/clrgit/constrain.
|
data/TODO
CHANGED
data/constrain.gemspec
CHANGED
@@ -26,7 +26,7 @@ Gem::Specification.new do |spec|
|
|
26
26
|
|
27
27
|
Constrain works with ruby-2 (and maybe ruby-3)
|
28
28
|
}
|
29
|
-
spec.homepage = "
|
29
|
+
spec.homepage = "https://github.com/clrgit/constrain/"
|
30
30
|
spec.required_ruby_version = Gem::Requirement.new(">= 2.3.0")
|
31
31
|
|
32
32
|
spec.metadata["homepage_uri"] = spec.homepage
|
data/lib/constrain/version.rb
CHANGED
data/lib/constrain.rb
CHANGED
@@ -5,43 +5,92 @@ module Constrain
|
|
5
5
|
class Error < StandardError; end
|
6
6
|
|
7
7
|
# Raised if types doesn't match a class expression
|
8
|
-
class
|
9
|
-
def initialize(value, exprs, msg = nil)
|
8
|
+
class MatchError < Error
|
9
|
+
def initialize(value, exprs, msg = nil, unwind: 0)
|
10
10
|
super msg || "Expected #{value.inspect} to match #{Constrain.fmt_exprs(exprs)}"
|
11
11
|
end
|
12
12
|
end
|
13
13
|
|
14
|
-
|
15
|
-
|
16
|
-
|
14
|
+
def self.included base
|
15
|
+
base.extend ClassMethods
|
16
|
+
end
|
17
|
+
|
18
|
+
# See Constrain.constrain
|
17
19
|
def constrain(value, *exprs)
|
20
|
+
Constrain.do_constrain(value, *exprs)
|
21
|
+
end
|
22
|
+
|
23
|
+
# Like #constrain but returns true/false to indicate the result instead of
|
24
|
+
# raising an exception
|
25
|
+
def constrain?(value, expr)
|
26
|
+
Constrain.do_constrain?(value, expr)
|
27
|
+
end
|
28
|
+
|
29
|
+
# :call-seq:
|
30
|
+
# constrain(value, *class-expressions, unwind: 0)
|
31
|
+
# constrain(value, *values, unwind: 0)
|
32
|
+
#
|
33
|
+
# Check that value matches one of the class expressions. Raises a
|
34
|
+
# Constrain::Error if the expression is invalid and a Constrain::MatchError if
|
35
|
+
# the value doesn't match. The exception's backtrace skips :unwind number of
|
36
|
+
# entries
|
37
|
+
def self.constrain(value, *exprs)
|
38
|
+
do_constrain(value, *exprs)
|
39
|
+
end
|
40
|
+
|
41
|
+
# Return true if the value matches the class expression. Raises a
|
42
|
+
# Constrain::Error if the expression is invalid
|
43
|
+
#
|
44
|
+
# TODO: Allow *exprs
|
45
|
+
def self.constrain?(value, expr)
|
46
|
+
do_constrain?(value, expr)
|
47
|
+
end
|
48
|
+
|
49
|
+
module ClassMethods
|
50
|
+
# See Constrain.constrain
|
51
|
+
def constrain(*args) Constrain.do_constrain(*args) end
|
52
|
+
|
53
|
+
# See Constrain.constrain?
|
54
|
+
def constrain?(*args) Constrain.do_constrain?(*args) end
|
55
|
+
end
|
56
|
+
|
57
|
+
# unwind is automatically incremented by one because ::do_constrain is always
|
58
|
+
# called from one of the other constrain methods
|
59
|
+
#
|
60
|
+
def self.do_constrain(value, *exprs)
|
61
|
+
if exprs.last.is_a?(Hash)
|
62
|
+
unwind = (exprs.last.delete(:unwind) || 0) + 1
|
63
|
+
!exprs.last.empty? or exprs.pop
|
64
|
+
else
|
65
|
+
unwind = 1
|
66
|
+
end
|
18
67
|
msg = exprs.pop if exprs.last.is_a?(String)
|
19
68
|
begin
|
20
69
|
!exprs.empty? or raise Error, "Empty class expression"
|
21
|
-
exprs.any? { |expr| Constrain.
|
70
|
+
exprs.any? { |expr| Constrain.do_constrain?(value, expr) } or
|
71
|
+
raise MatchError.new(value, exprs, msg, unwind: unwind)
|
22
72
|
rescue Error => ex
|
23
|
-
ex.set_backtrace(caller[1..-1])
|
73
|
+
ex.set_backtrace(caller[1 + unwind..-1])
|
24
74
|
raise
|
25
75
|
end
|
76
|
+
value
|
26
77
|
end
|
27
78
|
|
28
|
-
|
29
|
-
# Constrain::Error if the expression is invalid
|
30
|
-
def self.check(value, expr)
|
79
|
+
def self.do_constrain?(value, expr)
|
31
80
|
case expr
|
32
81
|
when Class, Module
|
33
82
|
value.is_a?(expr)
|
34
83
|
when Array
|
35
84
|
!expr.empty? or raise Error, "Empty array"
|
36
|
-
value.is_a?(Array) && value.all? { |elem| expr.any? { |e|
|
85
|
+
value.is_a?(Array) && value.all? { |elem| expr.any? { |e| Constrain.constrain?(elem, e) } }
|
37
86
|
when Hash
|
38
87
|
value.is_a?(Hash) && value.all? { |key, value|
|
39
88
|
expr.any? { |key_expr, value_expr|
|
40
89
|
[[key, key_expr], [value, value_expr]].all? { |value, expr|
|
41
90
|
if expr.is_a?(Array) && (expr.size > 1 || expr.first.is_a?(Array))
|
42
|
-
expr.any? { |e|
|
91
|
+
expr.any? { |e| Constrain.do_constrain?(value, e) }
|
43
92
|
else
|
44
|
-
|
93
|
+
Constrain.constrain?(value, expr)
|
45
94
|
end
|
46
95
|
}
|
47
96
|
}
|
@@ -49,7 +98,7 @@ module Constrain
|
|
49
98
|
when Proc
|
50
99
|
expr.call(value)
|
51
100
|
else
|
52
|
-
|
101
|
+
expr === value
|
53
102
|
end
|
54
103
|
end
|
55
104
|
|
@@ -66,11 +115,12 @@ module Constrain
|
|
66
115
|
def self.fmt_expr(expr)
|
67
116
|
case expr
|
68
117
|
when Class, Module; expr.to_s
|
118
|
+
when Regexp; expr.to_s
|
69
119
|
when Array; "[" + expr.map { |expr| fmt_expr(expr) }.join(", ") + "]"
|
70
120
|
when Hash; "{" + expr.map { |k,v| "#{fmt_expr(k)} => #{fmt_expr(v)}" }.join(", ") + "}"
|
71
121
|
when Proc; "Proc@#{expr.source_location.first}:#{expr.source_location.last}"
|
72
122
|
else
|
73
|
-
raise Error, "Illegal expression"
|
123
|
+
raise Error, "Illegal expression: #{expr.inspect}"
|
74
124
|
end
|
75
125
|
end
|
76
126
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: constrain
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Claus Rasmussen
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-
|
11
|
+
date: 2021-09-20 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: simplecov
|
@@ -52,10 +52,10 @@ files:
|
|
52
52
|
- constrain.gemspec
|
53
53
|
- lib/constrain.rb
|
54
54
|
- lib/constrain/version.rb
|
55
|
-
homepage:
|
55
|
+
homepage: https://github.com/clrgit/constrain/
|
56
56
|
licenses: []
|
57
57
|
metadata:
|
58
|
-
homepage_uri:
|
58
|
+
homepage_uri: https://github.com/clrgit/constrain/
|
59
59
|
post_install_message:
|
60
60
|
rdoc_options: []
|
61
61
|
require_paths:
|
@@ -71,7 +71,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
71
71
|
- !ruby/object:Gem::Version
|
72
72
|
version: '0'
|
73
73
|
requirements: []
|
74
|
-
rubygems_version: 3.
|
74
|
+
rubygems_version: 3.2.26
|
75
75
|
signing_key:
|
76
76
|
specification_version: 4
|
77
77
|
summary: Dynamic in-file type checking
|