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 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