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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b37ca58d51de33d6c5d6810c5b8e4b77e0d1d19355d4e381241b923f44918817
4
- data.tar.gz: c64f19d94bb2e4dae6e4fbaf61a74d1fb6caee56f58a9006915a5749ee35b906
3
+ metadata.gz: e48891d02cd81470d1f83543f6db1f7da99d75f3afe05cdc57b2c1e63d7a2ff9
4
+ data.tar.gz: dd37d53ea1080db393871ede639a653f985cc38373314d8b23b5013f3d8f9803
5
5
  SHA512:
6
- metadata.gz: bd6114ec7c7b5516c409ef41beae62d979c582f8f53c149634a1fc8ed8b4c595a7b3fd2d12d07a08fd2b7b587515adef890e6f68cdf6dd897d6ef79e8bad9c50
7
- data.tar.gz: 9eec4bc8dac6976bbe2eb22e45bcf57589b5f1e04092596e1cb99f658c0b9144a68182eab73e0e5ae3eb9be9bb9434184f8c71122aaddba284d15f90cac4600e
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'll typically include the Constrain module and use the #constrain method to chech values:
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
- include Constrain
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
- # f takes a String and an array of Integer objects
32
- def f(a, b)
33
- constrain a, String
34
- constrain b, [Integer]
35
- end
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
- The constrain instance method raises a Constrain::TypeError if the value
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
- ### Class Expressions
110
+ NilClass is a valid argument and can be used to allow nil values:
43
111
 
44
- Constrain#constrain and Constrain::check use class expressions composed of
45
- Class objects, Proc objects, or arrays and hashes of class objects. Class
46
- objects match if the value is an instance of the class:
112
+ ```ruby
113
+ constrain nil, Integer # Failure
114
+ constrain nil, Integer, NilClass # Success
115
+ ```
47
116
 
48
- constrain 42, Integer # Success
49
- constrain 42, String # Failure
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
- Note that NilClass and TrueClass and FalseClass are valid arguments and allows
52
- you to do value comparison for those types:
120
+ ```ruby
121
+ constrain true, TrueClass, FalseClass # Success
122
+ constrain false, TrueClass, FalseClass # Success
123
+ constrain nil, TrueClass, FalseClass # Failure
124
+ ```
53
125
 
54
- constrain nil, Integer # Failure
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
- proc = lambda { |value| value > 1 }
60
- constrain 42, proc # Success
61
- constrain 0, proc # Failure
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
- Proc objects can check every aspect of an object and but you should not overuse
64
- them as `Constrain` is throught of as a poor-man's type checker. More elaborate
65
- constraints should be checked explicitly
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
- constrain [42], [Integer] # Success
70
- constrain [42], [String] # Failure
156
+ ```ruby
157
+ constrain [42], [Integer] # Success
158
+ constrain [42], [String] # Failure
159
+ ```
71
160
 
72
161
  Arrays can be nested
73
162
 
74
- constrain [[42]], [[Integer]]
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
- Note that arrays are treated specially in hashes
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
- constrain({"str" => 42}, String => Integer) # Success
82
- constrain({"str" => 42}, String => String) # Failure
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
- constrain({ sym: 42 }, [Symbol, String] => Integer) # Success
93
- constrain({ [sym] => 42 }, [Symbol, String] => Integer) # Failure
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
- constrain({ [sym] => 42 }, [[Symbol, String]] => Integer) # Success
204
+ ```ruby
205
+ constrain({ [sym] => 42 }, [[Symbol, String]] => Integer) # Success
206
+ ```
99
207
 
100
208
  ## Contributing
101
209
 
data/TODO ADDED
@@ -0,0 +1,5 @@
1
+
2
+ o Class | Class syntax
3
+ o attr_reader :variable, ClassExpr
4
+
5
+ + 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
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
- return if exprs.any? { |expr| Constrain.check(value, expr) }
19
- error = TypeError.new(value, exprs)
20
- error.set_backtrace(caller[1..-1])
21
- raise error
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.check(value, expr)
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| check(elem, 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| check(value, e) }
53
+ expr.any? { |e| constrain?(value, e) }
39
54
  else
40
- check(value, expr)
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}"
@@ -1,3 +1,3 @@
1
1
  module Constrain
2
- VERSION = "0.1.0"
2
+ VERSION = "0.2.1"
3
3
  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.0
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-15 00:00:00.000000000 Z
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: http://www.nowhere.com/
55
+ homepage: https://github.com/clrgit/constrain/
55
56
  licenses: []
56
57
  metadata:
57
- homepage_uri: http://www.nowhere.com/
58
+ homepage_uri: https://github.com/clrgit/constrain/
58
59
  post_install_message:
59
60
  rdoc_options: []
60
61
  require_paths: