constrain 0.1.3 → 0.3.0
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 +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
|