predicated 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.markdown +9 -0
- data/lib/predicated.rb +6 -0
- data/lib/predicated/constrain.rb +64 -0
- data/lib/predicated/evaluate.rb +75 -0
- data/lib/predicated/from/callable_object.rb +79 -0
- data/lib/predicated/from/ruby_string.rb +65 -0
- data/lib/predicated/from/url_fragment.rb +97 -0
- data/lib/predicated/gem_check.rb +34 -0
- data/lib/predicated/predicate.rb +92 -0
- data/lib/predicated/print.rb +50 -0
- data/lib/predicated/selector.rb +55 -0
- data/lib/predicated/to/arel.rb +35 -0
- data/lib/predicated/to/sentence.rb +91 -0
- data/lib/predicated/version.rb +3 -0
- data/test/constrain_test.rb +77 -0
- data/test/enumerable_test.rb +60 -0
- data/test/equality_test.rb +21 -0
- data/test/evaluate_test.rb +144 -0
- data/test/from/callable_object_test.rb +102 -0
- data/test/from/ruby_string_test.rb +135 -0
- data/test/from/url_fragment_parser_test.rb +116 -0
- data/test/from/url_fragment_test.rb +37 -0
- data/test/print_test.rb +65 -0
- data/test/selector_test.rb +82 -0
- data/test/suite.rb +4 -0
- data/test/test_helper.rb +42 -0
- data/test/test_helper_with_wrong.rb +5 -0
- data/test/to/arel_test.rb +45 -0
- data/test/to/sentence_test.rb +83 -0
- metadata +109 -0
data/README.markdown
ADDED
@@ -0,0 +1,9 @@
|
|
1
|
+
Predicated is a simple predicate model for Ruby.
|
2
|
+
|
3
|
+
Tracker project:
|
4
|
+
[http://www.pivotaltracker.com/projects/95014](http://www.pivotaltracker.com/projects/95014)
|
5
|
+
|
6
|
+
|
7
|
+
Right now this project makes use of Wrong for assertions. Wrong uses this project. It's kind of neat in an eat-your-own-dogfood sense, but it's possible that this will be problematic over time (particularly when changes in this project cause assertions to behave differently - if even temporarily).
|
8
|
+
|
9
|
+
A middle ground is to make "from ruby string" and "from callable object" use minitest asserts, since these are the "interesting" parts of Predicated relied on by Wrong.
|
data/lib/predicated.rb
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
module Predicated
|
2
|
+
class Constraints
|
3
|
+
def initialize
|
4
|
+
@constraints = []
|
5
|
+
end
|
6
|
+
|
7
|
+
def add(constraint)
|
8
|
+
@constraints << constraint
|
9
|
+
self
|
10
|
+
end
|
11
|
+
|
12
|
+
def check(whole_predicate)
|
13
|
+
result = ConstraintCheckResult.new
|
14
|
+
|
15
|
+
@constraints.collect do |constraint|
|
16
|
+
whole_predicate.select(*constraint.selectors).collect do |predicate, ancestors|
|
17
|
+
if ! constraint.check(predicate, ancestors)
|
18
|
+
result.violation(constraint, predicate)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
result
|
24
|
+
end
|
25
|
+
|
26
|
+
def ==(other)
|
27
|
+
@constraints == other.instance_variable_get("@constraints".to_sym)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
class Constraint
|
32
|
+
attr_reader :name, :selectors
|
33
|
+
def initialize(args)
|
34
|
+
@name = args[:name]
|
35
|
+
@selectors = args[:selectors] || [:all]
|
36
|
+
@check_that = args[:check_that]
|
37
|
+
end
|
38
|
+
|
39
|
+
def check(predicate, ancestors)
|
40
|
+
@check_that.call(predicate, ancestors)
|
41
|
+
end
|
42
|
+
|
43
|
+
def ==(other)
|
44
|
+
@name == other.name && @selectors == other.selectors
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
class ConstraintCheckResult
|
49
|
+
attr_reader :violations
|
50
|
+
def initialize
|
51
|
+
@violations = {}
|
52
|
+
end
|
53
|
+
|
54
|
+
def pass?
|
55
|
+
@violations.empty?
|
56
|
+
end
|
57
|
+
|
58
|
+
def violation(constraint, predicate)
|
59
|
+
@violations[constraint] ||= []
|
60
|
+
@violations[constraint] << predicate
|
61
|
+
self
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
require "predicated/predicate"
|
2
|
+
|
3
|
+
module Predicated
|
4
|
+
|
5
|
+
|
6
|
+
class Operation
|
7
|
+
attr_reader :method_sym
|
8
|
+
|
9
|
+
def initialize(left, method_sym, right)
|
10
|
+
super(left, right)
|
11
|
+
@method_sym = method_sym
|
12
|
+
end
|
13
|
+
|
14
|
+
def evaluate
|
15
|
+
left.send(@method_sym, *right)
|
16
|
+
end
|
17
|
+
|
18
|
+
def ==(other)
|
19
|
+
super && method_sym==other.method_sym
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
|
24
|
+
class Equal < Operation; def initialize(left, right); super(left, :==, right); end end
|
25
|
+
class LessThan < Operation; def initialize(left, right); super(left, :<, right); end end
|
26
|
+
class GreaterThan < Operation; def initialize(left, right); super(left, :>, right); end end
|
27
|
+
class LessThanOrEqualTo < Operation; def initialize(left, right); super(left, :<=, right); end end
|
28
|
+
class GreaterThanOrEqualTo < Operation; def initialize(left, right); super(left, :>=, right); end end
|
29
|
+
|
30
|
+
class Call < Operation
|
31
|
+
def self.shorthand
|
32
|
+
:Call
|
33
|
+
end
|
34
|
+
|
35
|
+
def initialize(left, method_sym, right=[])
|
36
|
+
super
|
37
|
+
end
|
38
|
+
|
39
|
+
def inspect
|
40
|
+
"Call(#{self.send(:part_inspect,left)}.#{method_sym.to_s}(#{self.send(:part_inspect, right)}))"
|
41
|
+
end
|
42
|
+
end
|
43
|
+
Predicate.module_eval(%{
|
44
|
+
def Call(left_object, method_sym, right_args=[])
|
45
|
+
::Predicated::Call.new(left_object, method_sym, right_args)
|
46
|
+
end
|
47
|
+
})
|
48
|
+
|
49
|
+
module Container
|
50
|
+
private
|
51
|
+
def boolean_or_evaluate(thing)
|
52
|
+
if thing.is_a?(FalseClass)
|
53
|
+
false
|
54
|
+
elsif thing.is_a?(TrueClass)
|
55
|
+
true
|
56
|
+
else
|
57
|
+
thing.evaluate
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
class And
|
63
|
+
include Container
|
64
|
+
def evaluate
|
65
|
+
boolean_or_evaluate(left) && boolean_or_evaluate(right)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
class Or
|
70
|
+
include Container
|
71
|
+
def evaluate
|
72
|
+
boolean_or_evaluate(left) || boolean_or_evaluate(right)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
require "predicated/predicate"
|
2
|
+
require "predicated/from/ruby_string"
|
3
|
+
|
4
|
+
#Procs and lambdas are "callable objects"
|
5
|
+
|
6
|
+
module Predicated
|
7
|
+
|
8
|
+
require_gem_version("ParseTree", "3.0.5", "parse_tree")
|
9
|
+
|
10
|
+
module Predicate
|
11
|
+
|
12
|
+
#hrm
|
13
|
+
def self.from_callable_object(context_or_callable_object=nil, context=nil, &block)
|
14
|
+
callable_object = nil
|
15
|
+
|
16
|
+
if context_or_callable_object.is_a?(Binding) || context_or_callable_object.nil?
|
17
|
+
context = context_or_callable_object
|
18
|
+
callable_object = block
|
19
|
+
else
|
20
|
+
callable_object = context_or_callable_object
|
21
|
+
end
|
22
|
+
|
23
|
+
context ||= callable_object.binding
|
24
|
+
|
25
|
+
from_ruby_string(TranslateToRubyString.convert(callable_object), context)
|
26
|
+
end
|
27
|
+
|
28
|
+
module TranslateToRubyString
|
29
|
+
#see http://stackoverflow.com/questions/199603/how-do-you-stringize-serialize-ruby-code
|
30
|
+
def self.convert(callable_object)
|
31
|
+
temp_class = Class.new
|
32
|
+
temp_class.class_eval do
|
33
|
+
define_method :serializable, &callable_object
|
34
|
+
end
|
35
|
+
ruby_string = Ruby2Ruby.translate(temp_class, :serializable)
|
36
|
+
ruby_string.sub(/^def serializable\n /, "").sub(/\nend$/, "")
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
#see http://gist.github.com/321038
|
41
|
+
# # Monkey-patch to have Ruby2Ruby#translate with r2r >= 1.2.3, from
|
42
|
+
# # http://seattlerb.rubyforge.org/svn/ruby2ruby/1.2.2/lib/ruby2ruby.rb
|
43
|
+
class ::Ruby2Ruby < ::SexpProcessor
|
44
|
+
def self.translate(klass_or_str, method = nil)
|
45
|
+
sexp = ParseTree.translate(klass_or_str, method)
|
46
|
+
unifier = Unifier.new
|
47
|
+
unifier.processors.each do |p|
|
48
|
+
p.unsupported.delete :cfunc # HACK
|
49
|
+
end
|
50
|
+
sexp = unifier.process(sexp)
|
51
|
+
self.new.process(sexp)
|
52
|
+
end
|
53
|
+
|
54
|
+
#sconover - 7/2010 - monkey-patch
|
55
|
+
#{1=>2}=={1=>2}
|
56
|
+
#The right side was having its braces cut off because of
|
57
|
+
#special handling of hashes within arglists within the seattlerb code.
|
58
|
+
#I tried to fork r2r and add a test, but a lot of other tests
|
59
|
+
#broke, and I just dont understand the test in ruby2ruby.
|
60
|
+
#So I'm emailing the author...
|
61
|
+
def process_hash(exp)
|
62
|
+
result = []
|
63
|
+
until exp.empty?
|
64
|
+
lhs = process(exp.shift)
|
65
|
+
rhs = exp.shift
|
66
|
+
t = rhs.first
|
67
|
+
rhs = process rhs
|
68
|
+
rhs = "(#{rhs})" unless [:lit, :str].include? t # TODO: verify better!
|
69
|
+
|
70
|
+
result << "#{lhs} => #{rhs}"
|
71
|
+
end
|
72
|
+
|
73
|
+
return "{ #{result.join(', ')} }"
|
74
|
+
end
|
75
|
+
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
require "predicated/predicate"
|
2
|
+
require "predicated/evaluate"
|
3
|
+
|
4
|
+
module Predicated
|
5
|
+
|
6
|
+
require_gem_version("ruby_parser", "2.0.4")
|
7
|
+
require_gem_version("ruby2ruby", "1.2.4")
|
8
|
+
|
9
|
+
module Predicate
|
10
|
+
def self.from_ruby_string(ruby_predicate_string, context=binding())
|
11
|
+
sexp = RubyParser.new.process(ruby_predicate_string.strip)
|
12
|
+
SexpToPredicate.new(context).convert(sexp)
|
13
|
+
end
|
14
|
+
|
15
|
+
class SexpToPredicate
|
16
|
+
SIGN_TO_PREDICATE_CLASS = {
|
17
|
+
:== => Equal,
|
18
|
+
:> => GreaterThan,
|
19
|
+
:< => LessThan,
|
20
|
+
:>= => GreaterThanOrEqualTo,
|
21
|
+
:<= => LessThanOrEqualTo,
|
22
|
+
}
|
23
|
+
|
24
|
+
def initialize(context)
|
25
|
+
@context = context
|
26
|
+
end
|
27
|
+
|
28
|
+
def convert(sexp)
|
29
|
+
first_element = sexp.first
|
30
|
+
if first_element == :block
|
31
|
+
#eval all the top lines and then treat the last one as a predicate
|
32
|
+
body_sexps = sexp.sexp_body.to_a
|
33
|
+
body_sexps.slice(0..-2).each do |upper_sexp|
|
34
|
+
eval(Ruby2Ruby.new.process(upper_sexp), @context)
|
35
|
+
end
|
36
|
+
convert(body_sexps.last)
|
37
|
+
elsif first_element == :call
|
38
|
+
sym, left_sexp, method_sym, right_sexp = sexp
|
39
|
+
left = eval(Ruby2Ruby.new.process(left_sexp), @context)
|
40
|
+
right = eval(Ruby2Ruby.new.process(right_sexp), @context)
|
41
|
+
|
42
|
+
if operation_class=SIGN_TO_PREDICATE_CLASS[method_sym]
|
43
|
+
operation_class.new(left, right)
|
44
|
+
else
|
45
|
+
Call.new(left, method_sym, right)
|
46
|
+
end
|
47
|
+
elsif first_element == :and
|
48
|
+
sym, left, right = sexp
|
49
|
+
And.new(convert(left), convert(right))
|
50
|
+
elsif first_element == :or
|
51
|
+
sym, left, right = sexp
|
52
|
+
Or.new(convert(left), convert(right))
|
53
|
+
else
|
54
|
+
raise DontKnowWhatToDoWithThisSexpError.new(sexp)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
class DontKnowWhatToDoWithThisSexpError < StandardError
|
60
|
+
def initialize(sexp)
|
61
|
+
super("don't know what to do with #{sexp.inspect}")
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
require "predicated/predicate"
|
2
|
+
|
3
|
+
module Predicated
|
4
|
+
|
5
|
+
require_gem_version("treetop", "1.4.8")
|
6
|
+
|
7
|
+
module Predicate
|
8
|
+
def self.from_url_fragment(url_fragment_string)
|
9
|
+
TreetopUrlFragmentParser.new.parse(url_fragment_string).to_predicate
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
module TreetopUrlFragment
|
14
|
+
Treetop.load_from_string(%{
|
15
|
+
|
16
|
+
grammar TreetopUrlFragment
|
17
|
+
|
18
|
+
include Predicated::TreetopUrlFragment
|
19
|
+
|
20
|
+
rule or
|
21
|
+
( and "|" or <OrNode>) / and
|
22
|
+
end
|
23
|
+
|
24
|
+
rule and
|
25
|
+
( leaf "&" and <AndNode> ) / leaf
|
26
|
+
end
|
27
|
+
|
28
|
+
rule operation
|
29
|
+
unquoted_string sign unquoted_string <OperationNode>
|
30
|
+
end
|
31
|
+
|
32
|
+
rule parens
|
33
|
+
"(" or ")" <ParensNode>
|
34
|
+
end
|
35
|
+
|
36
|
+
|
37
|
+
rule leaf
|
38
|
+
operation / parens
|
39
|
+
end
|
40
|
+
|
41
|
+
rule unquoted_string
|
42
|
+
[0-9a-zA-Z]*
|
43
|
+
end
|
44
|
+
|
45
|
+
rule sign
|
46
|
+
('>=' / '<=' / '<' / '>' / '=' )
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
})
|
51
|
+
|
52
|
+
class OperationNode < Treetop::Runtime::SyntaxNode
|
53
|
+
def left_text; elements[0].text_value end
|
54
|
+
def sign_text; elements[1].text_value end
|
55
|
+
def right_text; elements[2].text_value end
|
56
|
+
|
57
|
+
SIGN_TO_PREDICATE_CLASS = {
|
58
|
+
"=" => Equal,
|
59
|
+
">" => GreaterThan,
|
60
|
+
"<" => LessThan,
|
61
|
+
">=" => GreaterThanOrEqualTo,
|
62
|
+
"<=" => LessThanOrEqualTo
|
63
|
+
}
|
64
|
+
|
65
|
+
def to_predicate
|
66
|
+
SIGN_TO_PREDICATE_CLASS[sign_text].new(left_text, right_text)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
class AndNode < Treetop::Runtime::SyntaxNode
|
71
|
+
def left; elements[0] end
|
72
|
+
def right; elements[2] end
|
73
|
+
|
74
|
+
def to_predicate
|
75
|
+
And.new(left.to_predicate, right.to_predicate)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
class OrNode < Treetop::Runtime::SyntaxNode
|
80
|
+
def left; elements[0] end
|
81
|
+
def right; elements[2] end
|
82
|
+
|
83
|
+
def to_predicate
|
84
|
+
Or.new(left.to_predicate, right.to_predicate)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
class ParensNode < Treetop::Runtime::SyntaxNode
|
89
|
+
def inner; elements[1] end
|
90
|
+
|
91
|
+
def to_predicate
|
92
|
+
inner.to_predicate
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Predicated
|
2
|
+
def self.require_gem_version(gem_name, minimum_version, require_name=gem_name)
|
3
|
+
unless Gem.available?(gem_name, Gem::Requirement.create(">= #{minimum_version}"))
|
4
|
+
raise %{
|
5
|
+
Gem: #{gem_name} >=#{minimum_version}
|
6
|
+
Does not appear to be installed. Please install it.
|
7
|
+
|
8
|
+
Predicated is built in a way that allows you to pick and
|
9
|
+
choose which features to use.
|
10
|
+
|
11
|
+
RubyGems has no way to specify optional dependencies,
|
12
|
+
therefore I've made the decision not to have Predicated
|
13
|
+
automatically depend into the various gems referenced
|
14
|
+
in from/to "extensions".
|
15
|
+
|
16
|
+
The cost here is that the gem install doesn't necessarily
|
17
|
+
"just work" for you out of the box. But in return you get
|
18
|
+
greater flexibility.
|
19
|
+
|
20
|
+
Notably, rails/arel unfortunately has a hard dependency
|
21
|
+
on Rails 3 activesupport, which requires ruby 1.8.7.
|
22
|
+
By making from/to dependencies optional, those with
|
23
|
+
no interest in arel can use Predicated in a wider
|
24
|
+
variety of environments.
|
25
|
+
|
26
|
+
For more discussion see:
|
27
|
+
http://stackoverflow.com/questions/2993335/rubygems-optional-dependencies
|
28
|
+
}
|
29
|
+
end
|
30
|
+
|
31
|
+
require require_name
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
require "predicated/gem_check"
|
2
|
+
require "predicated/selector"
|
3
|
+
|
4
|
+
module Predicated
|
5
|
+
def Predicate(&block)
|
6
|
+
result = nil
|
7
|
+
Module.new do
|
8
|
+
extend Predicate
|
9
|
+
result = instance_eval(&block)
|
10
|
+
end
|
11
|
+
result
|
12
|
+
end
|
13
|
+
|
14
|
+
class Binary
|
15
|
+
attr_accessor :left, :right
|
16
|
+
|
17
|
+
def initialize(left, right)
|
18
|
+
@left = left
|
19
|
+
@right = right
|
20
|
+
end
|
21
|
+
|
22
|
+
module FlipThroughMe
|
23
|
+
def each(ancestors=[], &block)
|
24
|
+
yield([self, ancestors])
|
25
|
+
ancestors_including_me = ancestors.dup + [self]
|
26
|
+
enumerate_side(@left, ancestors_including_me, &block)
|
27
|
+
enumerate_side(@right, ancestors_including_me, &block)
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
def enumerate_side(thing, ancestors)
|
32
|
+
thing.each(ancestors) { |item| yield(item) } if thing.is_a?(Enumerable)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
include FlipThroughMe
|
36
|
+
include Enumerable
|
37
|
+
|
38
|
+
module ValueEquality
|
39
|
+
def ==(other)
|
40
|
+
self.class == other.class &&
|
41
|
+
self.left == other.left &&
|
42
|
+
self.right == other.right
|
43
|
+
end
|
44
|
+
end
|
45
|
+
include ValueEquality
|
46
|
+
end
|
47
|
+
|
48
|
+
class Operation < Binary; end
|
49
|
+
|
50
|
+
module Predicate
|
51
|
+
extend Predicated::Selector
|
52
|
+
|
53
|
+
CLASS_INFO = [
|
54
|
+
[:And, :And, Class.new(Binary)],
|
55
|
+
[:Or, :Or, Class.new(Binary)],
|
56
|
+
[:Equal, :Eq, Class.new(Operation)],
|
57
|
+
[:LessThan, :Lt, Class.new(Operation)],
|
58
|
+
[:GreaterThan, :Gt, Class.new(Operation)],
|
59
|
+
[:LessThanOrEqualTo, :Lte, Class.new(Operation)],
|
60
|
+
[:GreaterThanOrEqualTo, :Gte, Class.new(Operation)]
|
61
|
+
]
|
62
|
+
|
63
|
+
#not great
|
64
|
+
base_selector_enumerable = SelectorEnumerable(
|
65
|
+
(CLASS_INFO.collect{|class_sym, sh, class_obj|class_obj} + [Binary, Operation]).
|
66
|
+
inject({:all => proc{|predicate, enumerable|true}}) do |h, class_obj|
|
67
|
+
h[class_obj] = proc{|predicate, enumerable|predicate.is_a?(class_obj)}
|
68
|
+
h
|
69
|
+
end
|
70
|
+
)
|
71
|
+
|
72
|
+
CLASS_INFO.each do |operation_class_name, shorthand, class_object|
|
73
|
+
Predicated.const_set(operation_class_name, class_object)
|
74
|
+
class_object.instance_variable_set("@shorthand".to_sym, shorthand)
|
75
|
+
class_object.class_eval do
|
76
|
+
|
77
|
+
def self.shorthand
|
78
|
+
@shorthand
|
79
|
+
end
|
80
|
+
|
81
|
+
include base_selector_enumerable
|
82
|
+
end
|
83
|
+
module_eval(%{
|
84
|
+
def #{shorthand}(left, right)
|
85
|
+
::Predicated::#{operation_class_name}.new(left, right)
|
86
|
+
end
|
87
|
+
})
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
require "predicated/print"
|