predicated 0.1.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.
- 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"
|