constrain 0.1.0 → 0.2.1
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 +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:
|