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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c176bf713350005e558d37a3b88b08f2a5f0ba37f0d2a829aa1d4b5dd1e8b0b4
4
- data.tar.gz: 02f69468f0130af3c702159b3cff2def10f1755916bc774e903f3d6cb8136fba
3
+ metadata.gz: fda0b95a5d43c197c9df1134d68dbe4fbc1f8f71ad9dc7a8e995daf729eaea2c
4
+ data.tar.gz: 2fe722e5085bc9b396e203b3f3514a1f123c18843e293f902392519ef6a3d5ed
5
5
  SHA512:
6
- metadata.gz: 5117883c01aa7658dd4adefcabb8af14c761090abb407d4141fd6f9321828d8f386f6dccba634d8e08af09bd893d8457d05495cbeba2a91f66d52af92e5c1b4e
7
- data.tar.gz: 5dc0238f65e4a6771b9a2bca452ab9eabb160c17ca32cec143a835bceb2296a317a661b7faa613a2c0148b3a3e236aef82e64351064a66b250ba146a8b9b5d36
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 but with a different syntax and only dynamic checks
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 class
46
+ have it available in all child classes
62
47
 
63
- The #constrain method has the following signature
48
+ ## Methods
64
49
 
65
- ```ruby
66
- constrain(value, *class-expressions, message = nil)
67
- ```
50
+ #### constrain(value, \*expressions, message = nil, unwind: 0)
68
51
 
69
- It checks that the value matches at least one of the class-expressions
70
- and raise a Constrain::TypeError if not. The error message can be customized by
71
- added the message argument. #constrain also raise a Constrain::Error exception
72
- if there is an error in the class expression. It is typically used to
73
- type-check parameters in methods
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
- Constrain also defines a #check class method with the signature
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
- ```ruby
78
- Constrain.check(value, *class-expression) -> true or false
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
- It matches value against the class expressions like #constrain but returns true
82
- or false as result
70
+ #### Constrain.constrain(value, \*expressions, message = nil, unwind: 0)
83
71
 
84
- ## Class Expressions
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
- ```ruby
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
- More than one class expression is allowed. It matches if at least one of the expressions match:
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
- #### nil, true and false
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
- NilClass is a valid argument and can be used to allow nil values:
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
- constrain nil, Integer # Failure
110
- constrain nil, Integer, NilClass # Success
96
+ def print_color(color)
97
+ constrain color, :red, :yellow, :green
98
+ ...
99
+ end
111
100
  ```
112
101
 
113
- Boolean values are a special case since ruby doesn't have a boolean type use a
114
- list to match for a boolean argument:
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
- constrain true, TrueClass, FalseClass # Success
118
- constrain false, TrueClass, FalseClass # Success
119
- constrain nil, TrueClass, FalseClass # Failure
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
- #### Proc objects
115
+ ### Class Expressions
123
116
 
124
- Proc objects are called with the value as argument and should return truish or falsy:
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, lambda { |value| value > 1 } # Success
128
- constrain 0, lambda { |value| value > 1 } # Failure
122
+ constrain 42, Integer # Success
123
+ constrain 42, Comparable # Success
124
+ constrain nil, Comparable # Failure
129
125
  ```
130
126
 
131
- 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:
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 0, Integer # Success
135
- constrain 0, lambda { |value| value > 1 } # Failure
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
@@ -1,5 +1,6 @@
1
1
 
2
2
  o Class | Class syntax
3
3
  o attr_reader :variable, ClassExpr
4
+ o 'constrain arg, :one_value, :another_value, 1, 2, 3'
4
5
 
5
6
  + constrain value, class-expr, "Error message"
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 = "http://www.nowhere.com/"
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
@@ -1,3 +1,3 @@
1
1
  module Constrain
2
- VERSION = "0.1.3"
2
+ VERSION = "0.3.0"
3
3
  end
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 TypeError < Error
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
- # Check that value matches one of the class expressions. Raises a
15
- # Constrain::Error if the expression is invalid and a Constrain::TypeError if
16
- # the value doesn't match
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.check(value, expr) } or raise TypeError.new(value, exprs, msg)
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
- # Return true if the value matches the class expression. Raises a
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| check(elem, 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| check(value, e) }
91
+ expr.any? { |e| Constrain.do_constrain?(value, e) }
43
92
  else
44
- check(value, expr)
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
- raise Error, "Illegal expression #{expr.inspect}"
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.1.3
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-05-17 00:00:00.000000000 Z
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: http://www.nowhere.com/
55
+ homepage: https://github.com/clrgit/constrain/
56
56
  licenses: []
57
57
  metadata:
58
- homepage_uri: http://www.nowhere.com/
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.1.4
74
+ rubygems_version: 3.2.26
75
75
  signing_key:
76
76
  specification_version: 4
77
77
  summary: Dynamic in-file type checking