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