constrain 0.1.0 → 0.2.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +144 -36
- data/TODO +5 -0
- data/constrain.gemspec +1 -1
- data/lib/constrain.rb +29 -14
- data/lib/constrain/version.rb +1 -1
- metadata +5 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e48891d02cd81470d1f83543f6db1f7da99d75f3afe05cdc57b2c1e63d7a2ff9
|
4
|
+
data.tar.gz: dd37d53ea1080db393871ede639a653f985cc38373314d8b23b5013f3d8f9803
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f2b8bf75caa5936d24773c662f4f43da95820c7245770a1b07c80e82e397aaeea06927e749fb9e1d615daebee3be92ccfa0001ebf334a1d4ceab19d87c225c3c
|
7
|
+
data.tar.gz: '082302ac6cbd3a638f4175a2a91cb3e6bb73e75032d08e9eb21c1e41fff122a3909c983bdf1068bf383b1c32fd93e91808abc687301e446f9c4ef0b05ab2b21e'
|
data/README.md
CHANGED
@@ -4,6 +4,23 @@
|
|
4
4
|
typically used to check the type of method parameters and is an alternative to
|
5
5
|
using Ruby-3 .rbs files but with a different syntax and only dynamic checks
|
6
6
|
|
7
|
+
```ruby
|
8
|
+
include Constrain
|
9
|
+
|
10
|
+
# f takes a String and an array of Integer objects and raises otherwise
|
11
|
+
def f(a, b)
|
12
|
+
constrain a, String
|
13
|
+
constrain b, [Integer]
|
14
|
+
...
|
15
|
+
end
|
16
|
+
|
17
|
+
f("Hello", [1, 2]) # Doesn't raise
|
18
|
+
f("Hello", "world") # Boom
|
19
|
+
```
|
20
|
+
|
21
|
+
It is intended to be an aid in development only and to be deactivated in
|
22
|
+
production (TODO: Make it possible to deactivate)
|
23
|
+
|
7
24
|
Constrain works with ruby-2 (and maybe ruby-3)
|
8
25
|
|
9
26
|
## Installation
|
@@ -24,62 +41,149 @@ Or install it yourself as:
|
|
24
41
|
|
25
42
|
## Usage
|
26
43
|
|
27
|
-
You
|
44
|
+
You will typically include Constrain globally to have #constrain available everywhere
|
45
|
+
|
46
|
+
```ruby
|
47
|
+
require 'constrain'
|
48
|
+
|
49
|
+
# Include globally to make #constrain available everywhere
|
50
|
+
include Constrain
|
51
|
+
|
52
|
+
def f(a, b, c)
|
53
|
+
constrain a, Integer # An integer
|
54
|
+
constrain b, [Symbol, String] => Integer # Hash with String or Symbol keys
|
55
|
+
constrain c, [String], NilClass # Array of strings or nil
|
56
|
+
...
|
57
|
+
end
|
58
|
+
```
|
59
|
+
|
60
|
+
The alternative is to include the constrain Module in a common root class to
|
61
|
+
have it available in all child class
|
62
|
+
|
63
|
+
## Methods
|
64
|
+
|
65
|
+
#### constrain(value, \*class-expressions, message = nil, unwind: 0)
|
28
66
|
|
29
|
-
|
67
|
+
Return the given value if it matches at least one of the class-expressions and raise a
|
68
|
+
Constrain::TypeError if not. The error message can be customized by added the
|
69
|
+
message argument and a number of backtrace leves can be skipped by setting
|
70
|
+
:unwind option. By default the backtrace will refer to the point of the call of
|
71
|
+
\#constrain. \#constrain araises a Constrain::Error exception if there is
|
72
|
+
an error in the syntax of the class expression
|
30
73
|
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
74
|
+
\#constrain is typically used to type-check parameters in methods where you
|
75
|
+
want an exception if the parameters doesn't match the expected, but because it
|
76
|
+
returns the value if successful it can be used to check the validity of
|
77
|
+
variables in expressions too, eg. `return constrain(result_of_complex_computation, Integer)`
|
78
|
+
to check the return value of a method
|
79
|
+
|
80
|
+
|
81
|
+
#### Constrain.constrain?(value, \*class-expression) -> true or false
|
82
|
+
|
83
|
+
It matches value against the class expressions like #constrain but returns true
|
84
|
+
or false as result and can be used to handle complex type expressions
|
85
|
+
dynamically. It is made a class method to minimize namespace pollution
|
86
|
+
|
87
|
+
|
88
|
+
## Class Expressions
|
89
|
+
|
90
|
+
Constrain#constrain and Constrain::constrain? use class expressions composed of
|
91
|
+
class or module objects, Proc objects, or arrays and hashes of class expressions. Class or module
|
92
|
+
objects match if `value.is_a?(class_or_module)` returns true:
|
93
|
+
|
94
|
+
```ruby
|
95
|
+
constrain 42, Integer # Success
|
96
|
+
constrain 42, Comparable # Success
|
97
|
+
constrain nil, Comparable # Failure
|
98
|
+
```
|
99
|
+
|
100
|
+
More than one class expression is allowed. It matches if at least one of the expressions match:
|
101
|
+
|
102
|
+
```ruby
|
103
|
+
constrain "str", Symbol, String # Success
|
104
|
+
constrain :sym, Symbol, String # Success
|
105
|
+
constrain 42, Symbol, String # Failure
|
106
|
+
```
|
36
107
|
|
37
|
-
|
38
|
-
doesn't match the class expression. Constrain also defines the Constrain::check
|
39
|
-
class method that returns true/false depending on if the value match the
|
40
|
-
expression. Both methods raise a Constrain::Error if the expression is invalid
|
108
|
+
#### nil, true and false
|
41
109
|
|
42
|
-
|
110
|
+
NilClass is a valid argument and can be used to allow nil values:
|
43
111
|
|
44
|
-
|
45
|
-
|
46
|
-
|
112
|
+
```ruby
|
113
|
+
constrain nil, Integer # Failure
|
114
|
+
constrain nil, Integer, NilClass # Success
|
115
|
+
```
|
47
116
|
|
48
|
-
|
49
|
-
|
117
|
+
Boolean values are a special case since ruby doesn't have a boolean type use a
|
118
|
+
list to match for a boolean argument:
|
50
119
|
|
51
|
-
|
52
|
-
|
120
|
+
```ruby
|
121
|
+
constrain true, TrueClass, FalseClass # Success
|
122
|
+
constrain false, TrueClass, FalseClass # Success
|
123
|
+
constrain nil, TrueClass, FalseClass # Failure
|
124
|
+
```
|
53
125
|
|
54
|
-
|
55
|
-
constrain nil, Integer, NilClass # Success
|
126
|
+
#### Proc objects
|
56
127
|
|
57
128
|
Proc objects are called with the value as argument and should return truish or falsy:
|
58
129
|
|
59
|
-
|
60
|
-
|
61
|
-
|
130
|
+
```ruby
|
131
|
+
constrain 42, lambda { |value| value > 1 } # Success
|
132
|
+
constrain 0, lambda { |value| value > 1 } # Failure
|
133
|
+
```
|
134
|
+
|
135
|
+
Note that it is not possible to first match against a class expression and then use the proc object. You will either have to check for the type too in the proc object or make two calls to #constrain:
|
62
136
|
|
63
|
-
|
64
|
-
|
65
|
-
|
137
|
+
```ruby
|
138
|
+
constrain 0, Integer # Success
|
139
|
+
constrain 0, lambda { |value| value > 1 } # Failure
|
140
|
+
```
|
141
|
+
|
142
|
+
Proc objects are a little more verbose than checking the constraint without
|
143
|
+
\#constrain but it allows the use of the :unwind option to manipulate the
|
144
|
+
apparent origin in the source of the exception
|
145
|
+
|
146
|
+
Note that even though Proc objects can check every aspect of an object but you
|
147
|
+
should not overuse it because as checks becomes more complex they tend to
|
148
|
+
include business logic that should be kept in the production code. Constrain is
|
149
|
+
only thouhgt of as a tool to catch developer errors - not errors that stem from
|
150
|
+
corrupted data
|
151
|
+
|
152
|
+
#### Arrays
|
66
153
|
|
67
154
|
Arrays match if the value is an Array and all its element match the given class expression:
|
68
155
|
|
69
|
-
|
70
|
-
|
156
|
+
```ruby
|
157
|
+
constrain [42], [Integer] # Success
|
158
|
+
constrain [42], [String] # Failure
|
159
|
+
```
|
71
160
|
|
72
161
|
Arrays can be nested
|
73
162
|
|
74
|
-
|
163
|
+
```ruby
|
164
|
+
constrain [[42]], [[Integer]] # Success
|
165
|
+
constrain [42], [[Integer]] # Failure
|
166
|
+
```
|
167
|
+
|
168
|
+
More than one element class is allowed
|
169
|
+
|
170
|
+
```ruby
|
171
|
+
constrain ["str"], [String, Symbol] # Success
|
172
|
+
constrain [:sym], [String, Symbol] # Success
|
173
|
+
constrain [42], [String, Symbol] # Failure
|
174
|
+
```
|
175
|
+
|
176
|
+
Note that `[` ... `]` is treated specially in hashes
|
75
177
|
|
76
|
-
|
178
|
+
#### Hashes
|
77
179
|
|
78
180
|
Hashes match if value is a hash and every key/value pair match one of the given
|
79
181
|
key-class/value-class expressions:
|
80
182
|
|
81
|
-
|
82
|
-
|
183
|
+
```ruby
|
184
|
+
constrain({"str" => 42}, String => Integer) # Success
|
185
|
+
constrain({"str" => 42}, String => String) # Failure
|
186
|
+
```
|
83
187
|
|
84
188
|
Note that the parenthesis are needed because otherwise the Ruby parser would
|
85
189
|
interpret the hash as block argument to #constrain
|
@@ -89,13 +193,17 @@ expression match. List are annotated as an array but contains more than one
|
|
89
193
|
element so that `[String, Symbol]` matches either a String or a Symbol value
|
90
194
|
while `[String]` matches an array of String objects:
|
91
195
|
|
92
|
-
|
93
|
-
|
196
|
+
```ruby
|
197
|
+
constrain({ sym: 42 }, [Symbol, String] => Integer) # Success
|
198
|
+
constrain({ [sym] => 42 }, [Symbol, String] => Integer) # Failure
|
199
|
+
```
|
94
200
|
|
95
201
|
To specify an array of Symbol or String objects in hash keys or values, make
|
96
202
|
sure the list expression is enclosed in an array:
|
97
203
|
|
98
|
-
|
204
|
+
```ruby
|
205
|
+
constrain({ [sym] => 42 }, [[Symbol, String]] => Integer) # Success
|
206
|
+
```
|
99
207
|
|
100
208
|
## Contributing
|
101
209
|
|
data/TODO
ADDED
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.rb
CHANGED
@@ -6,38 +6,53 @@ module Constrain
|
|
6
6
|
|
7
7
|
# Raised if types doesn't match a class expression
|
8
8
|
class TypeError < Error
|
9
|
-
def initialize(value, exprs)
|
10
|
-
super "Expected #{value.inspect} to match #{Constrain.fmt_exprs(exprs)}"
|
9
|
+
def initialize(value, exprs, msg = nil, unwind: 0)
|
10
|
+
super msg || "Expected #{value.inspect} to match #{Constrain.fmt_exprs(exprs)}"
|
11
11
|
end
|
12
12
|
end
|
13
13
|
|
14
|
+
# :call-seq:
|
15
|
+
# constrain(value, *class-expressions, unwind: 0)
|
16
|
+
#
|
14
17
|
# Check that value matches one of the class expressions. Raises a
|
15
18
|
# Constrain::Error if the expression is invalid and a Constrain::TypeError if
|
16
|
-
# the value doesn't match
|
17
|
-
def constrain(value, *exprs)
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
19
|
+
# the value doesn't match. The exception's backtrace skips :unwind number of entries
|
20
|
+
def constrain(value, *exprs) #, unwind: 0)
|
21
|
+
if exprs.last.is_a?(Hash)
|
22
|
+
unwind = exprs.last.delete(:unwind) || 0
|
23
|
+
!exprs.last.empty? or exprs.pop
|
24
|
+
else
|
25
|
+
unwind = 0
|
26
|
+
end
|
27
|
+
msg = exprs.pop if exprs.last.is_a?(String)
|
28
|
+
begin
|
29
|
+
!exprs.empty? or raise Error, "Empty class expression"
|
30
|
+
exprs.any? { |expr| Constrain.constrain?(value, expr) } or
|
31
|
+
raise TypeError.new(value, exprs, msg, unwind: unwind)
|
32
|
+
rescue Error => ex
|
33
|
+
ex.set_backtrace(caller[1 + unwind..-1])
|
34
|
+
raise
|
35
|
+
end
|
36
|
+
value
|
22
37
|
end
|
23
38
|
|
24
39
|
# Return true if the value matches the class expression. Raises a
|
25
40
|
# Constrain::Error if the expression is invalid
|
26
|
-
def self.
|
41
|
+
def self.constrain?(value, expr)
|
27
42
|
case expr
|
28
|
-
when Class
|
43
|
+
when Class, Module
|
29
44
|
value.is_a?(expr)
|
30
45
|
when Array
|
31
46
|
!expr.empty? or raise Error, "Empty array"
|
32
|
-
value.is_a?(Array) && value.all? { |elem| expr.any? { |e|
|
47
|
+
value.is_a?(Array) && value.all? { |elem| expr.any? { |e| constrain?(elem, e) } }
|
33
48
|
when Hash
|
34
49
|
value.is_a?(Hash) && value.all? { |key, value|
|
35
50
|
expr.any? { |key_expr, value_expr|
|
36
51
|
[[key, key_expr], [value, value_expr]].all? { |value, expr|
|
37
52
|
if expr.is_a?(Array) && (expr.size > 1 || expr.first.is_a?(Array))
|
38
|
-
expr.any? { |e|
|
53
|
+
expr.any? { |e| constrain?(value, e) }
|
39
54
|
else
|
40
|
-
|
55
|
+
constrain?(value, expr)
|
41
56
|
end
|
42
57
|
}
|
43
58
|
}
|
@@ -61,7 +76,7 @@ module Constrain
|
|
61
76
|
#
|
62
77
|
def self.fmt_expr(expr)
|
63
78
|
case expr
|
64
|
-
when Class; expr.to_s
|
79
|
+
when Class, Module; expr.to_s
|
65
80
|
when Array; "[" + expr.map { |expr| fmt_expr(expr) }.join(", ") + "]"
|
66
81
|
when Hash; "{" + expr.map { |k,v| "#{fmt_expr(k)} => #{fmt_expr(v)}" }.join(", ") + "}"
|
67
82
|
when Proc; "Proc@#{expr.source_location.first}:#{expr.source_location.last}"
|
data/lib/constrain/version.rb
CHANGED
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.1
|
4
|
+
version: 0.2.1
|
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-05-
|
11
|
+
date: 2021-05-22 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: simplecov
|
@@ -46,15 +46,16 @@ files:
|
|
46
46
|
- Gemfile
|
47
47
|
- README.md
|
48
48
|
- Rakefile
|
49
|
+
- TODO
|
49
50
|
- bin/console
|
50
51
|
- bin/setup
|
51
52
|
- constrain.gemspec
|
52
53
|
- lib/constrain.rb
|
53
54
|
- lib/constrain/version.rb
|
54
|
-
homepage:
|
55
|
+
homepage: https://github.com/clrgit/constrain/
|
55
56
|
licenses: []
|
56
57
|
metadata:
|
57
|
-
homepage_uri:
|
58
|
+
homepage_uri: https://github.com/clrgit/constrain/
|
58
59
|
post_install_message:
|
59
60
|
rdoc_options: []
|
60
61
|
require_paths:
|