predicated 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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,6 @@
1
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
2
+
3
+ require "predicated/gem_check"
4
+ require "predicated/predicate"
5
+ require "predicated/evaluate"
6
+ require "predicated/constrain"
@@ -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"