constraint 0.1 → 0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,83 @@
1
+ #!/usr/bin/env ruby
2
+ # queens.rb
3
+ # @Author: Thomas Link (samul@web.de)
4
+ # @License: GPL (see http://www.gnu.org/licenses/gpl.txt)
5
+ # @Created: 11-Jun-2005.
6
+ # @Last Change: 12-Jun-2005.
7
+ # @Revision: 0.290
8
+
9
+ require 'constraint'
10
+
11
+ QUEEN_MIN = 1
12
+ QUEEN_MAX = 5
13
+ MAX_BOARD_N = (QUEEN_MAX ** QUEEN_MAX) - 1
14
+
15
+ class QueensBoard < Constraint::CArray
16
+ attr_reader :curr_board_n
17
+
18
+ def new_constrained
19
+ @boards = []
20
+ @masks = []
21
+ @curr_board_n = 0
22
+ super
23
+ end
24
+
25
+ def generate_constrained_value(value)
26
+ if (@curr_board_n += 1) <= MAX_BOARD_N
27
+ return board(@curr_board_n)
28
+ else
29
+ super
30
+ end
31
+ end
32
+
33
+ def board(pos=nil)
34
+ if pos
35
+ unless (b = @boards[pos])
36
+ b = @boards[pos] = []
37
+ (QUEEN_MAX - 1).downto(0) do |x|
38
+ y, pos = pos.divmod(QUEEN_MAX ** x)
39
+ b << y
40
+ end
41
+ end
42
+ return b
43
+ else
44
+ return @constrained_value
45
+ end
46
+ end
47
+
48
+ def good?(x, y)
49
+ @constrained_value.each_with_index do |ox, oy|
50
+ if y == oy
51
+ next
52
+ elsif x == ox or ((y - oy).abs == (x - ox).abs)
53
+ return false
54
+ end
55
+ end
56
+ return true
57
+ end
58
+
59
+ or_constraint('Good') do |qb, queen|
60
+ rv = true
61
+ qb.constrained_value.each_with_index do |x, y|
62
+ if !qb.good?(x, y)
63
+ rv = false
64
+ break
65
+ end
66
+ end
67
+ rv
68
+ end
69
+ end
70
+
71
+
72
+ if __FILE__ == $0
73
+ b = QueensBoard.new
74
+
75
+ puts "First solution:"
76
+ p b.next_constrained_value!.board.collect {|e| e}
77
+
78
+ puts "All Solutions:"
79
+ for board in b.collect_constrained_values_until {|e| b.curr_board_n >= MAX_BOARD_N}
80
+ p board.board.collect {|e| e}
81
+ end
82
+ end
83
+
@@ -0,0 +1,82 @@
1
+ #!/usr/bin/env ruby
2
+ # queens.rb
3
+ # @Author: Thomas Link (samul@web.de)
4
+ # @License: GPL (see http://www.gnu.org/licenses/gpl.txt)
5
+ # @Created: 11-Jun-2005.
6
+ # @Last Change: 12-Jun-2005.
7
+ # @Revision: 0.388
8
+
9
+ require 'constraint'
10
+
11
+ QUEEN_MIN = 1
12
+ QUEEN_MAX = 5
13
+ MAX_BOARD_N = (QUEEN_MAX ** QUEEN_MAX) - 1
14
+
15
+ class QueensBoard < Constraint::CArray
16
+ attr_reader :curr_board_n
17
+
18
+ def new_constrained
19
+ @boards = []
20
+ @masks = []
21
+ @curr_board_n = 0
22
+ super
23
+ end
24
+
25
+ def generate_constrained_value(value)
26
+ if (@curr_board_n += 1) <= MAX_BOARD_N
27
+ return board(@curr_board_n)
28
+ else
29
+ super(@curr_board_n)
30
+ end
31
+ end
32
+
33
+ def board(pos=nil)
34
+ if pos
35
+ unless (@constrained_value = @boards[pos])
36
+ @constrained_value = []
37
+ (QUEEN_MAX - 1).downto(0) do |x|
38
+ y, pos = pos.divmod(QUEEN_MAX ** x)
39
+ self << [x, y]
40
+ end
41
+ @boards[pos] = @constrained_value
42
+ end
43
+ end
44
+ return @constrained_value
45
+ end
46
+
47
+ or_constraint('Good') do |qb, queen|
48
+ x, y = queen
49
+ rv = true
50
+ qb.constrained_value.each do |other_queen|
51
+ if queen.equal?(other_queen)
52
+ next
53
+ end
54
+ ox, oy = other_queen
55
+ if y == oy or x == ox or ((y - oy).abs == (x - ox).abs)
56
+ rv = false
57
+ break
58
+ end
59
+ end
60
+ rv
61
+ end
62
+ end
63
+
64
+
65
+ if __FILE__ == $0
66
+ b = QueensBoard.new
67
+
68
+ puts "First solution:"
69
+ p b.next_constrained_value!.board.collect {|x, y| y}
70
+
71
+ puts "All Solutions:"
72
+ for board in b.collect_constrained_values_until {|e| b.curr_board_n >= MAX_BOARD_N - 1}
73
+ b = board.board
74
+ # b.sort do |q1, q2|
75
+ # x1, y1 = q1
76
+ # x2, y2 = q2
77
+ # x1 <=> x2
78
+ # end
79
+ p b.collect {|x, y| y}
80
+ end
81
+ end
82
+
@@ -0,0 +1,85 @@
1
+ #!/usr/bin/env ruby
2
+ # queens.rb
3
+ # @Author: Thomas Link (samul@web.de)
4
+ # @License: GPL (see http://www.gnu.org/licenses/gpl.txt)
5
+ # @Created: 11-Jun-2005.
6
+ # @Last Change: 12-Jun-2005.
7
+ # @Revision: 0.223
8
+
9
+ require 'constraint'
10
+
11
+ QUEEN_MIN = 1
12
+ QUEEN_MAX = 5
13
+
14
+ class QueensBoard < Constraint::Shell
15
+ def new_constrained
16
+ @boards = []
17
+ @masks = []
18
+ @max_board = (QUEEN_MAX ** QUEEN_MAX) - 1
19
+ nil
20
+ end
21
+
22
+ def process_constrained_value
23
+ if @constrained_value
24
+ super
25
+ end
26
+ end
27
+
28
+ def generate_constrained_value(value)
29
+ if !value
30
+ return 0
31
+ elsif value < @max_board
32
+ return value + 1
33
+ else
34
+ super
35
+ end
36
+ end
37
+
38
+ def board(pos=@constrained_value)
39
+ unless (b = @boards[pos])
40
+ b = @boards[pos] = []
41
+ QUEEN_MAX.downto(1) do |x|
42
+ xx, pos = pos.divmod(QUEEN_MAX ** (x - 1))
43
+ b << xx
44
+ end
45
+ end
46
+ return b
47
+ end
48
+
49
+ def strike_any?(x, y, board)
50
+ board.each_with_index do |oy, ox|
51
+ if x == ox
52
+ next
53
+ elsif y == oy or ((y - oy).abs == (x - ox).abs)
54
+ return true
55
+ end
56
+ end
57
+ return false
58
+ end
59
+
60
+ or_constraint('Good') do |qb, board|
61
+ b = qb.board(board)
62
+ rv = true
63
+ b.each_with_index do |y, i|
64
+ if qb.strike_any?(i, y, b)
65
+ rv = false
66
+ break
67
+ end
68
+ end
69
+ rv
70
+ end
71
+ end
72
+
73
+
74
+ if __FILE__ == $0
75
+ b = QueensBoard.new
76
+
77
+ puts "First solution:"
78
+ p b.next_constrained_value!.board.collect {|e| e}
79
+
80
+ puts "All Solutions:"
81
+ for board in b.collect_constrained_values_until {|e| e >= (QUEEN_MAX ** QUEEN_MAX) - 2}
82
+ p board.board.collect {|e| e}
83
+ end
84
+ end
85
+
@@ -0,0 +1,5 @@
1
+ #!/bin/bash
2
+ for i in queens*; do
3
+ echo ------$i
4
+ time ruby $i
5
+ done
@@ -2,110 +2,114 @@
2
2
  # @Author: Thomas Link (samul AT web.de)
3
3
  # @License: GPL (see http://www.gnu.org/licenses/gpl.txt)
4
4
  # @Created: 23-Mai-2005.
5
- # @Last Change: 09-Jun-2005.
6
- # @Revision: 607
5
+ # @Last Change: 14-Jun-2005.
6
+ # @Revision: 986
7
7
 
8
-
9
- class ConstraintViolation < Exception
10
- end
8
+ require 'forwardable'
11
9
 
12
10
 
13
- # If CONSTRAINT_DISABLE is set to true *before* loading this library,
11
+ # If CONSTRAINT_DISABLE is set to true _before_ loading this library,
14
12
  # constraint checking is turned off.
15
13
  CONSTRAINT_DISABLE = false unless defined?(CONSTRAINT_DISABLE)
16
14
 
17
15
 
18
16
  module Constraint
19
- VERSION = '0.1'
17
+ VERSION = '0.2'
18
+
19
+
20
+ class Violation < Exception
21
+ end
20
22
 
21
- # The base constraint class. Always evaluates to true.
22
- class NilConstraint
23
+ class NoMoreValues < Violation
24
+ end
25
+
26
+ # The base constraint class.
27
+ class SingleConstraint
23
28
  # shell:: The Shell class containing this constraint
24
29
  # name:: This constraint's name
25
30
  # description:: Optional constraint description
26
31
  # block:: The constraint that must evaluate to true in order for this constraint to succeed
27
32
  def initialize(shell, name, description=nil, &block)
28
- @shell = shell
29
- @other = shell.constraints
33
+ # @other = shell.constraints
30
34
  @name = name
31
- @description = description
32
35
  @this = block
33
36
  end
34
37
 
35
- # Return true if +args+ complies with @this constraint.
36
- def evaluate(object)
37
- begin
38
- rv = @this.call(object)
39
- rescue Exception => e
40
- @shell.log_constraint_exception(e)
41
- end
42
- begin
43
- if @other
44
- return evaluate_control(rv, object)
45
- elsif rv
46
- return object
47
- else
48
- return evaluate(handle_violation(object))
49
- end
50
- rescue ConstraintViolation => e
51
- raise e
38
+ def evaluate(invoker, object)
39
+ if @this.call(invoker, object)
40
+ object
41
+ else
42
+ invoker.handle_constraint_violation(@name, object)
52
43
  end
53
44
  end
54
-
55
- # Delegate to Shell#handle_constraint_violation.
56
- def handle_violation(object)
57
- return @shell.handle_constraint_violation(@name, object)
58
- end
59
-
60
- private
61
- # Combine constraints. In the base class, always evaluate to
62
- # true.
63
- def evaluate_control(value, object)
64
- raise "Subclass responsibility"
65
- # value ? object : handle_violation(object)
66
- end
67
45
  end
68
-
69
- class OrConstraint < NilConstraint
70
- private
71
- # Return true if +value+ or the @other constraint are true.
72
- def evaluate_control(value, object)
73
- value ? object : @other.evaluate(object)
46
+
47
+ # All constraints must succeed.
48
+ class AndConstraint < Array
49
+ def evaluate(invoker, object)
50
+ for a in self
51
+ object = a.evaluate(invoker, object)
52
+ end
53
+ object
74
54
  end
75
55
  end
76
-
77
- class AndConstraint < NilConstraint
78
- private
79
- # Return true if +value+ and the @other constraint are true.
80
- def evaluate_control(value, object)
81
- return value ? @other.evaluate(object) : handle_violation(object)
56
+
57
+ # At least one constraint must succeed.
58
+ class OrConstraint < Array
59
+ def evaluate(invoker, object)
60
+ c = nil
61
+ for o in self
62
+ begin
63
+ return o.evaluate(invoker, object)
64
+ rescue Constraint::Violation => e
65
+ c = e
66
+ end
67
+ end
68
+ raise c
82
69
  end
83
70
  end
84
-
71
+
85
72
  module Helper
86
73
  # The current class's/object's head constraint.
87
74
  attr_reader :constraints
88
- # A hash of constraint descriptions.
89
- attr_reader :descriptions
75
+ # A hash of constraint constaint_descriptions.
76
+ attr_reader :constaint_descriptions
77
+ # An array of constraint handlers
78
+ attr_reader :constaint_handlers
79
+ # An array of additional attributes that should be copied when
80
+ # replicating a shell
81
+ attr_reader :constraint_attributes
90
82
 
91
- # Define an OrConstraint. If the new constraint succeedes, all
92
- # previous constraints become obsolete.
83
+ # Define an OrConstraint. Either this or the previous constraint have to succeed.
93
84
  # name:: This constraint's name
94
85
  # description:: Optional constraint description
95
86
  # block:: The constraint that must evaluate to true in order for this constraint to succeed
96
87
  def or_constraint(name, desc=nil, &block)
97
88
  describe_constraint(name, desc)
98
- @constraints = OrConstraint.new(self, name, desc, &block)
89
+ @constraints = @constraints.dup
90
+ constraint = SingleConstraint.new(self, name, desc, &block)
91
+ if @constraints.empty?
92
+ @constraints << constraint
93
+ else
94
+ last = @constraints.last
95
+ case last
96
+ when OrConstraint
97
+ last << constraint
98
+ else
99
+ @constraints[-1] = OrConstraint.new([last, constraint])
100
+ end
101
+ end
99
102
  end
100
103
 
101
- # Define an AndConstraint. This and all previous constraints
104
+ # Define an SingleConstraint. This and all previous constraints
102
105
  # have to succeed.
103
106
  # name:: This constraint's name
104
107
  # description:: Optional constraint description
105
108
  # block:: The constraint that must evaluate to true in order for this constraint to succeed
106
109
  def and_constraint(name, desc=nil, &block)
107
110
  describe_constraint(name, desc)
108
- @constraints = AndConstraint.new(self, name, desc, &block)
111
+ @constraints = @constraints.dup
112
+ @constraints << SingleConstraint.new(self, name, desc, &block)
109
113
  end
110
114
 
111
115
  # If $VERBOSE is set, print the exception to $stderr.
@@ -115,50 +119,94 @@ module Constraint
115
119
  end
116
120
  end
117
121
 
118
- # Handle constraint violations. Can be overwritten by subclasses
119
- # in order to rescue the constraint. The return value will
120
- # replace the original object.
121
- # name:: The constraint's name that didn't succeed
122
- # object:: The object that caused the violation
123
- def handle_constraint_violation(name, object)
124
- c = @descriptions.collect {|name, desc| desc}.join(', ')
125
- raise ConstraintViolation,
126
- "#{self.class}: #{object.class} didn't meet constraints (#{c}): #{object.inspect}",
127
- caller[5..-1]
122
+ # Define a new constraint handler. The block takes a
123
+ # continuation as its first argument. If the amendment of the
124
+ # object isn't successful, the handler must throw an
125
+ # Constraint::Violation exception.
126
+ #
127
+ # Example:
128
+ #
129
+ # enum = EvenInteger.new(2)
130
+ # enum.and_constraint('LT10') {|enum, n| n < 10}
131
+ # enum.on_constraint_violation('LT10') do |o, n|
132
+ # n > 10 ? 10 : 0
133
+ # end
134
+ #
135
+ # *names:: The constraints names
136
+ # block:: The handler (argument: object)
137
+ def on_constraint_violation(*names, &block)
138
+ @constaint_handlers = @constaint_handlers.dup
139
+ names.each do |name|
140
+ @constaint_handlers[name] ||= []
141
+ @constaint_handlers[name] << block
142
+ end
128
143
  end
129
144
 
145
+ def constraint_attr(name, val)
146
+ (@constraint_attributes = @constraint_attributes.dup) << name
147
+ instance_eval %{
148
+ class << self
149
+ attr_accessor :#{name}
150
+ end
151
+ attr_accessor :#{name}
152
+ }
153
+ instance_variable_set("@#{name}", val)
154
+ end
155
+
156
+ # Copy needed class/object variables.
157
+ def replicate_constraints(predecessor)
158
+ @constaint_descriptions = predecessor.constaint_descriptions
159
+ @constaint_handlers = predecessor.constaint_handlers
160
+ @constraints = predecessor.constraints
161
+ for name in (@constraint_attributes = predecessor.constraint_attributes)
162
+ nname = "@#{name}"
163
+ instance_variable_set(nname, predecessor.instance_variable_get(nname))
164
+ end
165
+ end
166
+
130
167
  private
131
168
  # Copy constraint-related class-local variables from +klass+ to self.
132
- def inherit_constraint(klass)
133
- @descriptions = klass.descriptions.dup
134
- @constraints = klass.constraints
135
- end
169
+ # def inherit_constraint(klass)
170
+ # @constaint_descriptions = klass.constaint_descriptions
171
+ # @constaint_handlers = klass.constaint_handlers
172
+ # @constraints = klass.constraints
173
+ # end
136
174
 
137
175
  # Register a constraint description. If no description is
138
176
  # provided, the +name+ is used.
139
177
  # name:: This constraint's name
140
178
  # desc:: Optional constraint description
141
179
  def describe_constraint(name, desc=nil)
142
- @descriptions[name] = (desc || name)
180
+ @constaint_descriptions[name] = (desc || name)
143
181
  end
144
182
  end
145
183
 
146
184
  # If CONSTRAINT_DISABLE is set to true *before* loading this library,
147
185
  # constraint checking is turned off.
148
186
  class Shell
187
+ extend Forwardable
188
+
149
189
  # The actual value that is covered by the Shell.
150
190
  attr_reader :constrained_value
151
191
 
152
- @descriptions = {}
153
- @constraints = nil
192
+ @constaint_descriptions = {}
193
+ @constaint_handlers = {}
194
+ # @constraints = nil
195
+ @constraints = AndConstraint.new
196
+ @constraint_attributes = []
154
197
 
155
198
  class << self
156
199
  include Helper
157
-
200
+
201
+ def delegate_to_constrained_value(meth, alias_name)
202
+ class_eval %{alias #{alias_name} #{meth}}
203
+ def_delegator(:@constrained_value, meth)
204
+ end
205
+
158
206
  def inherited(subclass)
159
207
  parent = self
160
208
  subclass.class_eval do
161
- inherit_constraint(parent)
209
+ replicate_constraints(parent)
162
210
  end
163
211
  end
164
212
 
@@ -174,12 +222,12 @@ module Constraint
174
222
  # Wrap +methods+ so that some arguments are filtered through
175
223
  # #check_constraints. This is useful in two situations:
176
224
  #
177
- # * if filtering the return value would be inefficient --
225
+ # * if filtering the return value would be inefficient --
178
226
  # e.g. with methods performing trivial Array operations
179
227
  # where the return value will comply with the constraints
180
228
  # if its argument do.
181
229
  #
182
- # * if the return value of the method call is not an
230
+ # * if the return value of the method call is not an
183
231
  # instance of the actual value's class (only then will the
184
232
  # value's integrity be checked)
185
233
  #
@@ -187,7 +235,6 @@ module Constraint
187
235
  #
188
236
  # methods:: Method names as symbols
189
237
  # block:: Select the arguments that should be filtered
190
-
191
238
  def with_arguments_constrained(*methods, &block)
192
239
  unless CONSTRAINT_DISABLE
193
240
  methods.each do |method|
@@ -215,24 +262,29 @@ module Constraint
215
262
 
216
263
  include Helper
217
264
 
218
- def initialize(core=nil, predecessor=nil)
219
- if predecessor
220
- @descriptions = predecessor.descriptions
221
- @constraints = predecessor.constraints
222
- else
223
- @descriptions = self.class.descriptions.dup
224
- @constraints = self.class.constraints
225
- @constraints = @constraints.dup if @constraints
226
- end
227
-
228
- if core
229
- @constrained_value = core
230
- else
231
- @constrained_value = new_constrained
232
- end
265
+ delegate_to_constrained_value(:inspect, :inspect_constraint_shell)
266
+ delegate_to_constrained_value(:=~, :match_constraint_shell)
267
+ delegate_to_constrained_value(:instance_eval, :constraint_shell_eval)
233
268
 
269
+ # delegate_to_constrained_value(:tainted?, :tainted_constraint_shell?)
270
+ # delegate_to_constrained_value(:taint, :taint_constraint_shell)
271
+ # delegate_to_constrained_value(:untaint, :untaint_constraint_shell)
272
+
273
+ delegate_to_constrained_value(:===, :case_equal_constraint_shell)
274
+ # def ===(other)
275
+ # if other.kind_of?(Shell)
276
+ # super
277
+ # else
278
+ # @constrained_value === other
279
+ # end
280
+ # end
281
+
282
+ def initialize(core=nil, predecessor=nil, &block)
283
+ predecessor ||= self.class
284
+ replicate_constraints(predecessor)
285
+ @constrained_value = core || new_constrained
234
286
  unless CONSTRAINT_DISABLE
235
- process_constrained_value {|e| check_constraints(e)}
287
+ check_constrained_value
236
288
  end
237
289
  end
238
290
 
@@ -244,12 +296,86 @@ module Constraint
244
296
  replicate_constraint_shell(super)
245
297
  end
246
298
 
247
- # Construct a new instance.
299
+ # Construct a new instance. By default, this method calls
300
+ # generate_constrained_value(nil).
248
301
  def new_constrained
249
- # nil
250
- raise 'Subclass responsibility'
302
+ generate_constrained_value(nil)
303
+ # raise 'Subclass responsibility'
304
+ end
305
+
306
+ # Generate the next value for #next_constrained_value.
307
+ def generate_constrained_value(value)
308
+ raise Constraint::NoMoreValues, "No more values: #{value}"
309
+ end
310
+
311
+ # "Increase" the current value to the next valid one (see
312
+ # #generate_constrained_value). Modify the object in-place.
313
+ def next_constrained_value!(value=nil)
314
+ begin
315
+ cv = @constrained_value
316
+ if value
317
+ @constrained_value = value
318
+ end
319
+ begin
320
+ while (@constrained_value = generate_constrained_value(@constrained_value))
321
+ check_constrained_value
322
+ return self
323
+ end
324
+ rescue Constraint::NoMoreValues => e
325
+ raise e
326
+ rescue Constraint::Violation => e
327
+ retry
328
+ end
329
+ raise_constraint_violation(nil, value, "No more values: #{value}")
330
+ rescue Exception => e
331
+ @constrained_value = cv
332
+ raise e
333
+ end
334
+ end
335
+
336
+ # Return an object with an increased value (see
337
+ # #generate_constrained_value).
338
+ def next_constrained_value(value=nil)
339
+ self.clone.next_constrained_value!(value)
340
+ end
341
+
342
+ # Return an array of constrained values starting from the
343
+ # current value until a value for which block is true. Requires
344
+ # #generate_constrained_value to be defined.
345
+ def collect_constrained_values_while(&block)
346
+ collect_constrained_values_control do |val, acc|
347
+ if (rv = block.call(val))
348
+ acc << self.dup
349
+ next_constrained_value!
350
+ end
351
+ rv
352
+ end
353
+ end
354
+
355
+ # Return an array of constrained values starting from the
356
+ # current value until a value for which block is true. Requires
357
+ # #generate_constrained_value to be defined.
358
+ def collect_constrained_values_until(&block)
359
+ collect_constrained_values_control do |val, acc|
360
+ if (rv = !block.call(val))
361
+ acc << self.dup
362
+ next_constrained_value!
363
+ end
364
+ rv
365
+ end
251
366
  end
252
367
 
368
+ # Collect all values. Shell#generate_constrained_value must
369
+ # provide a means to decide when no more values should be
370
+ # generated.
371
+ def collect_constrained_values
372
+ collect_constrained_values_control do |val, acc|
373
+ acc << self.dup
374
+ next_constrained_value!
375
+ true
376
+ end
377
+ end
378
+
253
379
  # Filters @constrained_value through the supplied block (usually a
254
380
  # call to #check_constraints) and collects the output in
255
381
  # @constrained_value).
@@ -258,6 +384,10 @@ module Constraint
258
384
  @constrained_value = yield @constrained_value
259
385
  end
260
386
 
387
+ def check_constrained_value
388
+ process_constrained_value {|e| check_constraints(e)}
389
+ end
390
+
261
391
  if CONSTRAINT_DISABLE
262
392
  def check_constraints(object)
263
393
  object
@@ -266,7 +396,7 @@ module Constraint
266
396
  # Check if +object+ complies with @constraints.
267
397
  def check_constraints(object)
268
398
  if @constraints
269
- return @constraints.evaluate(object)
399
+ return @constraints.evaluate(self, object)
270
400
  else
271
401
  return object
272
402
  end
@@ -288,7 +418,7 @@ module Constraint
288
418
  # Shell.with_arguments_constrained and friends.
289
419
  #
290
420
  # new_value:: The result from the method call
291
- # [block]:: Optional, called @constrained_value is +new_value+
421
+ # block:: Optional, called @constrained_value is +new_value+
292
422
  def with_constraints(new_value)
293
423
  if new_value.equal?(@constrained_value)
294
424
  yield if block_given?
@@ -300,6 +430,25 @@ module Constraint
300
430
  end
301
431
  end
302
432
 
433
+ # Handle constraint violations. Can be overwritten by subclasses
434
+ # in order to rescue the constraint. The return value will
435
+ # replace the original object.
436
+ # name:: The constraint's name that didn't succeed
437
+ # object:: The object that caused the violation
438
+ def handle_constraint_violation(name, object)
439
+ handlers = @constaint_handlers[name]
440
+ if handlers
441
+ for h in handlers
442
+ begin
443
+ return h.call(self, object)
444
+ rescue Constraint::Violation
445
+ next
446
+ end
447
+ end
448
+ end
449
+ raise_constraint_violation(name, object)
450
+ end
451
+
303
452
  if CONSTRAINT_DISABLE
304
453
  def method_missing(method, *args, &block)
305
454
  with_constraints(@constrained_value.send(method, *args, &block))
@@ -313,7 +462,7 @@ module Constraint
313
462
  def method_missing(method, *args, &block)
314
463
  # $stderr.puts "Constraint::Shell: Delegate method: #{method}" if $DEBUG
315
464
  with_constraints(@constrained_value.send(method, *args, &block)) do
316
- process_constrained_value {|e| check_constraints(e)}
465
+ check_constrained_value
317
466
  end
318
467
  end
319
468
  end
@@ -324,12 +473,47 @@ module Constraint
324
473
  end
325
474
 
326
475
  private
327
- # Some kind of semi-deep copy.
328
- def replicate_constraint_shell(new_instance)
329
- cv = @constrained_value.clone
330
- new_instance.instance_eval do
476
+ # Eval block while it's yielding true. Maintain
477
+ # @constrained_value in case of an exception.
478
+ def collect_constrained_values_control(&block)
479
+ begin
480
+ acc = []
481
+ cv = @constrained_value
482
+ begin
483
+ while block.call(@constrained_value, acc)
484
+ end
485
+ rescue Constraint::Violation
486
+ end
487
+ return acc
488
+ ensure
331
489
  @constrained_value = cv
332
490
  end
491
+ end
492
+
493
+ # Raise a Constraint::Violation.
494
+ def raise_constraint_violation(name, object, msg=nil)
495
+ i = 0
496
+ caller.each_with_index do |e,l|
497
+ if e =~ /^(\w+:)?([^:].*?[\\\/])+?constraint.rb:\d+:/
498
+ i = l
499
+ end
500
+ end
501
+ msg ||= "#{object.class} didn't meet constraint '#{name}': #{object.inspect}"
502
+ raise Constraint::Violation, msg, caller[i+1..-1]
503
+ end
504
+
505
+ # Some kind of semi-deep copy. If @constrained_value can't be
506
+ # cloned (e.g., a Fixnum), the new_instance will be returned as
507
+ # such.
508
+ # new_instance:: A duplicate of Shell
509
+ def replicate_constraint_shell(new_instance)
510
+ begin
511
+ cv = @constrained_value.clone
512
+ new_instance.constraint_shell_eval do
513
+ @constrained_value = cv
514
+ end
515
+ rescue TypeError
516
+ end
333
517
  new_instance
334
518
  end
335
519
  end