ruva 1.0.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/.gitignore +19 -0
- data/.travis.yml +7 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README.md +143 -0
- data/Rakefile +2 -0
- data/lib/ruva.rb +73 -0
- data/lib/ruva/expression.rb +17 -0
- data/lib/ruva/expression/equal_greater_less.rb +61 -0
- data/lib/ruva/expression/expression_leaf_spec.rb +52 -0
- data/lib/ruva/expression/identifier_comparable.rb +53 -0
- data/lib/ruva/expression/is_between.rb +49 -0
- data/lib/ruva/expression/matches.rb +42 -0
- data/lib/ruva/expression/ruva_expression.citrus +112 -0
- data/lib/ruva/specification.rb +1 -0
- data/lib/ruva/specification/specification.rb +100 -0
- data/lib/ruva/version.rb +3 -0
- data/ruva.gemspec +23 -0
- data/spec/conditions/simple_condition.ruva +7 -0
- data/spec/expression_spec.rb +85 -0
- data/spec/helpers/expression_helper.rb +7 -0
- data/spec/inline_ruva_spec.rb +73 -0
- data/spec/normalize_indentation_spec.rb +22 -0
- data/spec/ruva_spec.rb +20 -0
- metadata +123 -0
data/.gitignore
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2012 Samuel Mueller
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,143 @@
|
|
1
|
+
# Ruva
|
2
|
+
|
3
|
+
[](http://travis-ci.org/ssmm/ruva)
|
4
|
+
[](https://codeclimate.com/github/ssmm/ruva)
|
5
|
+
|
6
|
+
Ruva is a simple utility to write conditions in a human readable manner.
|
7
|
+
|
8
|
+
Assume you have an object called `person` with an attribute `age = 23`:
|
9
|
+
|
10
|
+
```ruby
|
11
|
+
person = OpenStruct.new
|
12
|
+
person.age = 23
|
13
|
+
```
|
14
|
+
|
15
|
+
You write
|
16
|
+
|
17
|
+
```ruby
|
18
|
+
person.if "age is 23" do
|
19
|
+
stuff
|
20
|
+
end
|
21
|
+
```
|
22
|
+
|
23
|
+
You can also define more complex conditions:
|
24
|
+
|
25
|
+
any
|
26
|
+
name matches samuel
|
27
|
+
all
|
28
|
+
city matches zurich
|
29
|
+
profession matches /application engineer/
|
30
|
+
age is greater than or equal to 18
|
31
|
+
|
32
|
+
In plain ruby, the above condition translates to
|
33
|
+
|
34
|
+
```ruby
|
35
|
+
(person.name =~ /samuel/) ||
|
36
|
+
(person.city =~ /zurich/ && person.profession =~ /application engineer/ && person.age >= 18)
|
37
|
+
```
|
38
|
+
|
39
|
+
# Details
|
40
|
+
|
41
|
+
## Inline definition
|
42
|
+
|
43
|
+
### One liners
|
44
|
+
|
45
|
+
As in the example above you can define a short condition on one line:
|
46
|
+
|
47
|
+
```ruby
|
48
|
+
person.if "name matches daniel or jonas" do
|
49
|
+
stuff
|
50
|
+
end
|
51
|
+
```
|
52
|
+
|
53
|
+
You can also define an else statement:
|
54
|
+
|
55
|
+
```ruby
|
56
|
+
person.if("name matches daniel or jonas") do
|
57
|
+
stuff
|
58
|
+
end.else do
|
59
|
+
other_stuff
|
60
|
+
end
|
61
|
+
```
|
62
|
+
|
63
|
+
Note that you have to put the condition into brackets in this case.
|
64
|
+
|
65
|
+
### Multiline
|
66
|
+
|
67
|
+
You may define larger conditions like this:
|
68
|
+
|
69
|
+
```ruby
|
70
|
+
person.if "
|
71
|
+
any
|
72
|
+
name matches lisa or simon or samuel
|
73
|
+
age is between 18 and 28
|
74
|
+
city matches zurich or /st. gallen/
|
75
|
+
" do
|
76
|
+
stuff
|
77
|
+
end
|
78
|
+
```
|
79
|
+
|
80
|
+
The above definition says, that the condition is met if any of the three conditions defined under the
|
81
|
+
`any` keyword meet.
|
82
|
+
|
83
|
+
Currently supported are the keywords `all`, `any` and `none`.
|
84
|
+
|
85
|
+
## Separate definition
|
86
|
+
|
87
|
+
You can define the same condition in a file with ".ruva" as extension:
|
88
|
+
|
89
|
+
any
|
90
|
+
name matches lisa or simon or samuel
|
91
|
+
age is between 18 and 28
|
92
|
+
city matches zurich or /st. gallen/
|
93
|
+
|
94
|
+
Then you load it:
|
95
|
+
|
96
|
+
```ruby
|
97
|
+
condition = Ruva.read "your_condition"
|
98
|
+
```
|
99
|
+
|
100
|
+
At this point, ruva loads the condition into some kind of hirarchy, according to the
|
101
|
+
[Specification pattern](http://en.wikipedia.org/wiki/Specification_pattern), and you can invoke its
|
102
|
+
`evaluate` method to test any object you want against that condition:
|
103
|
+
|
104
|
+
```ruby
|
105
|
+
result = condition.evaluate(your_input)
|
106
|
+
```
|
107
|
+
|
108
|
+
## Expressions
|
109
|
+
|
110
|
+
#### is
|
111
|
+
|
112
|
+
age is 20
|
113
|
+
|
114
|
+
Acts the same as the `==` operator.
|
115
|
+
|
116
|
+
#### is between
|
117
|
+
|
118
|
+
age is between 20 and 30
|
119
|
+
|
120
|
+
Acts the same as [`between?`](http://www.ruby-doc.org/core-1.9.3/Comparable.html#method-i-between-3F).
|
121
|
+
|
122
|
+
#### is greater than (or equal to) / is less than (or equal to)
|
123
|
+
|
124
|
+
age is greater than 18
|
125
|
+
age is less than 18
|
126
|
+
age is greater than or equal to 18
|
127
|
+
age is less than or equal to 18
|
128
|
+
|
129
|
+
Acts the same as the `<, >, <=, >=` operators
|
130
|
+
|
131
|
+
#### matches
|
132
|
+
|
133
|
+
city matches zurich
|
134
|
+
city matches /new york/
|
135
|
+
|
136
|
+
Acts the same as the `=~` operator.
|
137
|
+
|
138
|
+
#### or / and
|
139
|
+
|
140
|
+
You can use `ors` and `ands` where it makes sense. For Example:
|
141
|
+
|
142
|
+
name matches lisa or simon or samuel
|
143
|
+
city matches z and u and r
|
data/Rakefile
ADDED
data/lib/ruva.rb
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
require 'indentation-parser'
|
2
|
+
|
3
|
+
require "ruva/version"
|
4
|
+
require "ruva/expression"
|
5
|
+
|
6
|
+
class Object
|
7
|
+
def if condition
|
8
|
+
raise unless block_given?
|
9
|
+
spec = Ruva.interpret(Ruva.normalize_indentation condition)
|
10
|
+
result = spec.evaluate self
|
11
|
+
yield if result.satisfied
|
12
|
+
Ruva::Else.new result.satisfied
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
module Ruva
|
17
|
+
|
18
|
+
def self.read file
|
19
|
+
interpret IO.read("#{file}.ruva")
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.interpret text
|
23
|
+
|
24
|
+
parser = IndentationParser.new do |p|
|
25
|
+
p.on /^all$/ do |parent, source, captures|
|
26
|
+
node = AndSpec.new
|
27
|
+
parent.append node
|
28
|
+
node
|
29
|
+
end
|
30
|
+
|
31
|
+
p.on /^any$/ do |parent, source, captures|
|
32
|
+
node = OrSpec.new
|
33
|
+
parent.append node
|
34
|
+
node
|
35
|
+
end
|
36
|
+
|
37
|
+
p.on /^none$/ do |parent, source, captures|
|
38
|
+
node = NotSpec.new
|
39
|
+
parent.append node
|
40
|
+
node
|
41
|
+
end
|
42
|
+
|
43
|
+
p.else do |parent, source|
|
44
|
+
node = RuvaExpression.parse(source)
|
45
|
+
parent.append node
|
46
|
+
node
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
spec = parser.read(text, AndSpec.new)
|
51
|
+
spec.value
|
52
|
+
end
|
53
|
+
|
54
|
+
def self.normalize_indentation str
|
55
|
+
if str.index(/^[ ]*$\n/) == 0
|
56
|
+
str.sub!(/^[ ]*$\n/, "")
|
57
|
+
end
|
58
|
+
captures = /^\A([ ]*)/.match str
|
59
|
+
first_indentation = captures[1]
|
60
|
+
str.gsub!(/^#{first_indentation}/, "")
|
61
|
+
str
|
62
|
+
end
|
63
|
+
|
64
|
+
class Else
|
65
|
+
def initialize satisfied
|
66
|
+
@satisfied = satisfied
|
67
|
+
end
|
68
|
+
def else
|
69
|
+
raise unless block_given?
|
70
|
+
yield unless @satisfied
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'citrus'
|
2
|
+
require 'ostruct'
|
3
|
+
require 'ruva/specification'
|
4
|
+
require 'ruva/expression/expression_leaf_spec'
|
5
|
+
require 'ruva/expression/equal_greater_less'
|
6
|
+
require 'ruva/expression/identifier_comparable'
|
7
|
+
require 'ruva/expression/is_between'
|
8
|
+
require 'ruva/expression/matches'
|
9
|
+
|
10
|
+
Citrus.require 'ruva/expression/ruva_expression'
|
11
|
+
|
12
|
+
module RuvaExpression
|
13
|
+
def self.parse expression
|
14
|
+
result = super expression
|
15
|
+
result.value
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
class GreaterSpec < ExpressionLeafSpec
|
2
|
+
def compare (comparable, value) value > comparable end
|
3
|
+
def satisfied_text() "greater than" end
|
4
|
+
def unsatisfied_text() "less than" end
|
5
|
+
end
|
6
|
+
|
7
|
+
class LessSpec < ExpressionLeafSpec
|
8
|
+
def compare (comparable, value) value < comparable end
|
9
|
+
def satisfied_text() "less than" end
|
10
|
+
def unsatisfied_text() "greater than" end
|
11
|
+
end
|
12
|
+
|
13
|
+
class GreaterEqualSpec < ExpressionLeafSpec
|
14
|
+
def compare (comparable, value) value >= comparable end
|
15
|
+
def satisfied_text() "greater than or equal to" end
|
16
|
+
def unsatisfied_text() "less than" end
|
17
|
+
end
|
18
|
+
|
19
|
+
class LessEqualSpec < ExpressionLeafSpec
|
20
|
+
def compare (comparable, value) value <= comparable end
|
21
|
+
def satisfied_text() "less than or equal to" end
|
22
|
+
def unsatisfied_text() "greater than" end
|
23
|
+
end
|
24
|
+
|
25
|
+
module EqualGreaterLess
|
26
|
+
def value
|
27
|
+
spec = create_spec
|
28
|
+
spec.set_validator { |value, *args|
|
29
|
+
compare @comparable, value
|
30
|
+
}
|
31
|
+
spec.set_reporting { |satisfied, value, *args|
|
32
|
+
comparable_string = @comparable.to_s
|
33
|
+
create_report_string "is #{satisfied_text} #{comparable_string}", "is #{unsatisfied_text} #{comparable_string}", satisfied, value
|
34
|
+
}
|
35
|
+
spec
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
module Greater include EqualGreaterLess
|
40
|
+
def create_spec
|
41
|
+
GreaterSpec.new(identifier.value, comparable.value)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
module Less include EqualGreaterLess
|
46
|
+
def create_spec
|
47
|
+
LessSpec.new(identifier.value, comparable.value)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
module GreaterOrEqual include EqualGreaterLess
|
52
|
+
def create_spec
|
53
|
+
GreaterEqualSpec.new(identifier.value, comparable.value)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
module LessOrEqual include EqualGreaterLess
|
58
|
+
def create_spec
|
59
|
+
LessEqualSpec.new(identifier.value, comparable.value)
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
class ExpressionLeafSpec < LeafSpec
|
2
|
+
def get_value *args
|
3
|
+
if (args.size == 1)
|
4
|
+
obj = args[0]
|
5
|
+
if (obj.is_a? Hash)
|
6
|
+
get_hash_value @name.split(".").reverse, obj
|
7
|
+
else
|
8
|
+
get_object_value @name.split(".").reverse, obj
|
9
|
+
end
|
10
|
+
else
|
11
|
+
raise "Unexpected input"
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def get_hash_value key_array, hash
|
16
|
+
if (key_array.size > 1)
|
17
|
+
obj = hash[key_array.pop.to_sym]
|
18
|
+
if (obj.is_a?(Hash))
|
19
|
+
get_hash_value(key_array, obj)
|
20
|
+
else
|
21
|
+
get_object_value(key_array, obj)
|
22
|
+
end
|
23
|
+
else
|
24
|
+
hash[key_array.pop.to_sym]
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def get_object_value key_array, obj
|
29
|
+
if (key_array.size > 1)
|
30
|
+
obj = obj.send key_array.pop
|
31
|
+
if (obj.is_a?(Hash))
|
32
|
+
get_hash_value(key_array, obj)
|
33
|
+
else
|
34
|
+
get_object_value(key_array, obj)
|
35
|
+
end
|
36
|
+
else
|
37
|
+
obj.send key_array.pop
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def create_typed_value value
|
42
|
+
case value
|
43
|
+
when Numeric then value
|
44
|
+
when String then "'#{value}'"
|
45
|
+
else value
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def create_report_string positive, negative, satisfied, value
|
50
|
+
@name.to_s + " #{create_typed_value value} " + (satisfied ? positive : negative)
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module Identifier
|
2
|
+
def value
|
3
|
+
to_s.strip
|
4
|
+
end
|
5
|
+
end
|
6
|
+
|
7
|
+
module Comparable
|
8
|
+
def value
|
9
|
+
to_s
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
module LastComparable
|
14
|
+
def value
|
15
|
+
Array.new << comparable
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
module IntComparable
|
20
|
+
def value
|
21
|
+
to_i
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
module FloatComparable
|
26
|
+
def value
|
27
|
+
to_f
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
module DateComparable
|
32
|
+
def value
|
33
|
+
Date.parse to_s
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
module RegexComparable
|
38
|
+
def value
|
39
|
+
Regexp.new regex.value
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
module TextComparable
|
44
|
+
def value
|
45
|
+
return to_s.strip
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
module Comparables
|
50
|
+
def value
|
51
|
+
subresult.value << comparable
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
def create_is_spec identifier, comparable
|
2
|
+
spec = ExpressionLeafSpec.new(identifier.value, comparable.value)
|
3
|
+
spec.set_validator { |value, *args|
|
4
|
+
@comparable == value
|
5
|
+
}
|
6
|
+
spec.set_reporting { |satisfied, value, *args|
|
7
|
+
comparable_string = @comparable.to_s
|
8
|
+
create_report_string "is #{comparable_string}", "is not #{comparable_string}", satisfied, value
|
9
|
+
}
|
10
|
+
spec
|
11
|
+
end
|
12
|
+
|
13
|
+
module Is
|
14
|
+
def value
|
15
|
+
create_is_spec identifier, comparable
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
module ManyIs
|
20
|
+
def value
|
21
|
+
spec = create_spec
|
22
|
+
comparables.value.reverse.each do |comparable|
|
23
|
+
new_spec = create_is_spec identifier, comparable
|
24
|
+
spec.append(new_spec)
|
25
|
+
end
|
26
|
+
spec
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
module OrIs include ManyIs
|
31
|
+
def create_spec
|
32
|
+
OrSpec.new
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
module IsBetween
|
37
|
+
def value
|
38
|
+
comparables = [from.value, to.value]
|
39
|
+
spec = ExpressionLeafSpec.new(identifier.value, comparables)
|
40
|
+
spec.set_validator { |value, *args|
|
41
|
+
value.between? @comparable[0], @comparable[1]
|
42
|
+
}
|
43
|
+
spec.set_reporting { |satisfied, value, *args|
|
44
|
+
comparable_string = "#{@comparable[0]} and #{@comparable[1]}"
|
45
|
+
create_report_string "is between #{comparable_string}", "is not between #{comparable_string}", satisfied, value
|
46
|
+
}
|
47
|
+
spec
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
def create_matches_spec identifier, comparable
|
2
|
+
comparable = comparable.value
|
3
|
+
regex = comparable.is_a?(Regexp) ? comparable : Regexp.new(comparable)
|
4
|
+
spec = ExpressionLeafSpec.new(identifier.value, regex)
|
5
|
+
spec.set_validator { |value, *args|
|
6
|
+
(@comparable =~ value) != nil
|
7
|
+
}
|
8
|
+
spec.set_reporting { |satisfied, value, *args|
|
9
|
+
comparable_string = "/#{@comparable.source}/"
|
10
|
+
create_report_string "matches #{comparable_string}", "does not match #{comparable_string}", satisfied, value
|
11
|
+
}
|
12
|
+
spec
|
13
|
+
end
|
14
|
+
|
15
|
+
module Matches
|
16
|
+
def value
|
17
|
+
create_matches_spec identifier, comparable
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
module ManyMatches
|
22
|
+
def value
|
23
|
+
spec = create_spec
|
24
|
+
comparables.value.reverse.each do |comparable|
|
25
|
+
new_spec = create_matches_spec identifier, comparable
|
26
|
+
spec.append(new_spec)
|
27
|
+
end
|
28
|
+
spec
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
module AndMatches include ManyMatches
|
33
|
+
def create_spec
|
34
|
+
AndSpec.new
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
module OrMatches include ManyMatches
|
39
|
+
def create_spec
|
40
|
+
OrSpec.new
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,112 @@
|
|
1
|
+
grammar RuvaExpression
|
2
|
+
|
3
|
+
rule root
|
4
|
+
(identifier matches comparables:(and_comparables)) <AndMatches>
|
5
|
+
|
|
6
|
+
(identifier matches comparables:(or_comparables)) <OrMatches>
|
7
|
+
|
|
8
|
+
(identifier matches comparable) <Matches>
|
9
|
+
|
|
10
|
+
(identifier is between from:(comparable) and to:(comparable)) <IsBetween>
|
11
|
+
|
|
12
|
+
(identifier is greater than or equal to comparable) <GreaterOrEqual>
|
13
|
+
|
|
14
|
+
(identifier is less than or equal to comparable) <LessOrEqual>
|
15
|
+
|
|
16
|
+
(identifier is greater than comparable) <Greater>
|
17
|
+
|
|
18
|
+
(identifier is less than comparable) <Less>
|
19
|
+
|
|
20
|
+
(identifier is comparables:(or_comparables)) <OrIs>
|
21
|
+
|
|
22
|
+
(identifier is comparable) <Is>
|
23
|
+
end
|
24
|
+
|
25
|
+
rule matches
|
26
|
+
'matches' space
|
27
|
+
end
|
28
|
+
|
29
|
+
rule is
|
30
|
+
'is' space
|
31
|
+
end
|
32
|
+
|
33
|
+
rule between
|
34
|
+
'between' space
|
35
|
+
end
|
36
|
+
|
37
|
+
rule greater
|
38
|
+
'greater' space
|
39
|
+
end
|
40
|
+
|
41
|
+
rule less
|
42
|
+
'less' space
|
43
|
+
end
|
44
|
+
|
45
|
+
rule than
|
46
|
+
'than' space
|
47
|
+
end
|
48
|
+
|
49
|
+
rule equal
|
50
|
+
'equal' space
|
51
|
+
end
|
52
|
+
|
53
|
+
rule to
|
54
|
+
'to' space
|
55
|
+
end
|
56
|
+
|
57
|
+
rule and
|
58
|
+
'and' space
|
59
|
+
end
|
60
|
+
|
61
|
+
rule or
|
62
|
+
'or' space
|
63
|
+
end
|
64
|
+
|
65
|
+
rule identifier
|
66
|
+
([a-zA-Z.]+ space) <Identifier>
|
67
|
+
end
|
68
|
+
|
69
|
+
rule comparable
|
70
|
+
(number_comparable | date_comparable | regex_comparable | text_comparable)
|
71
|
+
end
|
72
|
+
|
73
|
+
rule last_comparable
|
74
|
+
(comparable '') <LastComparable>
|
75
|
+
end
|
76
|
+
|
77
|
+
rule number_comparable
|
78
|
+
(int_comparable | float_comparable)
|
79
|
+
end
|
80
|
+
|
81
|
+
rule int_comparable
|
82
|
+
([0-9]+ space) <IntComparable>
|
83
|
+
end
|
84
|
+
|
85
|
+
rule float_comparable
|
86
|
+
([0-9]* '.' [0-9]+ space) <FloatComparable>
|
87
|
+
end
|
88
|
+
|
89
|
+
rule date_comparable
|
90
|
+
([0-9]2*2 '.' [0-9]2*2 '.' [0-9]4*4 space) <DateComparable>
|
91
|
+
end
|
92
|
+
|
93
|
+
rule regex_comparable
|
94
|
+
('/' regex:((!'/'!'\\' .) | '\\/' | '\\\\')* '/' space) <RegexComparable>
|
95
|
+
end
|
96
|
+
|
97
|
+
rule text_comparable
|
98
|
+
([^ ]+ space) <TextComparable>
|
99
|
+
end
|
100
|
+
|
101
|
+
rule and_comparables
|
102
|
+
(comparable and subresult:(and_comparables | last_comparable)) <Comparables>
|
103
|
+
end
|
104
|
+
|
105
|
+
rule or_comparables
|
106
|
+
(comparable or subresult:(or_comparables | last_comparable)) <Comparables>
|
107
|
+
end
|
108
|
+
|
109
|
+
rule space
|
110
|
+
[ \t]+ | /$/
|
111
|
+
end
|
112
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
require 'ruva/specification/specification'
|
@@ -0,0 +1,100 @@
|
|
1
|
+
class Spec
|
2
|
+
def initialize
|
3
|
+
@specs = Array.new
|
4
|
+
end
|
5
|
+
|
6
|
+
def append spec
|
7
|
+
@specs << spec
|
8
|
+
spec
|
9
|
+
end
|
10
|
+
|
11
|
+
def create_subreport spec, *args
|
12
|
+
if spec.is_a? Spec
|
13
|
+
spec.evaluate *args
|
14
|
+
else
|
15
|
+
result = spec.evaluate *args
|
16
|
+
message = spec.report result, *args
|
17
|
+
SpecReport.new result, message
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def evaluate *args
|
22
|
+
passed = initial_condition
|
23
|
+
report = SpecReport.new false, name
|
24
|
+
@specs.each do |spec|
|
25
|
+
subreport = create_subreport spec, *args
|
26
|
+
report.add_subreport subreport
|
27
|
+
if condition_change? subreport.satisfied
|
28
|
+
passed = !initial_condition
|
29
|
+
end
|
30
|
+
end
|
31
|
+
report.satisfied = passed
|
32
|
+
report
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
class AndSpec < Spec
|
37
|
+
def initial_condition() return true end
|
38
|
+
def name() "all" end
|
39
|
+
def condition_change?(satisfied) !satisfied end
|
40
|
+
end
|
41
|
+
|
42
|
+
class OrSpec < Spec
|
43
|
+
def initial_condition() return false end
|
44
|
+
def name() "any" end
|
45
|
+
def condition_change?(satisfied) satisfied end
|
46
|
+
end
|
47
|
+
|
48
|
+
class NotSpec < Spec
|
49
|
+
def initial_condition() return true end
|
50
|
+
def name() "none" end
|
51
|
+
def condition_change?(satisfied) satisfied end
|
52
|
+
end
|
53
|
+
|
54
|
+
class LeafSpec
|
55
|
+
def initialize name, args
|
56
|
+
@name = name
|
57
|
+
@comparable = args
|
58
|
+
end
|
59
|
+
|
60
|
+
def set_validator &block
|
61
|
+
@validator = block
|
62
|
+
end
|
63
|
+
|
64
|
+
def set_reporting &block
|
65
|
+
@reporting = block
|
66
|
+
end
|
67
|
+
|
68
|
+
def evaluate *args
|
69
|
+
instance_exec(get_value(*args), *args, &@validator)
|
70
|
+
end
|
71
|
+
|
72
|
+
def report satisfied, *args
|
73
|
+
instance_exec(satisfied, get_value(*args), *args, &@reporting)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
class SpecReport
|
78
|
+
|
79
|
+
attr_accessor :satisfied, :name, :input
|
80
|
+
|
81
|
+
def initialize satisfied, message
|
82
|
+
@satisfied = satisfied
|
83
|
+
@message = message
|
84
|
+
@subreports = Array.new
|
85
|
+
end
|
86
|
+
|
87
|
+
def add_subreport subreport
|
88
|
+
@subreports << subreport
|
89
|
+
end
|
90
|
+
|
91
|
+
def to_s s = ''
|
92
|
+
met = @satisfied ? "[+] " : "[-] "
|
93
|
+
output = s + met + @message
|
94
|
+
output += "\n"
|
95
|
+
@subreports.each do |subreport|
|
96
|
+
output += subreport.to_s(s + ' ')
|
97
|
+
end
|
98
|
+
output
|
99
|
+
end
|
100
|
+
end
|
data/lib/ruva/version.rb
ADDED
data/ruva.gemspec
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/ruva/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.name = "ruva"
|
6
|
+
gem.version = Ruva::VERSION
|
7
|
+
|
8
|
+
gem.authors = ["Samuel Mueller"]
|
9
|
+
gem.email = ["mueller.samu@gmail.com"]
|
10
|
+
gem.description = %q{Ruva is a simple utility to write conditions in a human readable manner.}
|
11
|
+
gem.summary = gem.description
|
12
|
+
gem.homepage = "https://github.com/ssmm/ruva"
|
13
|
+
|
14
|
+
gem.files = `git ls-files`.split($\)
|
15
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
16
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
17
|
+
gem.require_paths = ["lib"]
|
18
|
+
|
19
|
+
gem.add_dependency "citrus", "~> 2.4.1"
|
20
|
+
gem.add_dependency "indentation-parser"
|
21
|
+
|
22
|
+
gem.add_development_dependency "rspec", "~> 2.6"
|
23
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
require 'ruva/expression'
|
2
|
+
require 'helpers/expression_helper'
|
3
|
+
|
4
|
+
RSpec.configure do |c|
|
5
|
+
c.include ExpressionHelper
|
6
|
+
end
|
7
|
+
|
8
|
+
describe Ruva do
|
9
|
+
|
10
|
+
before :all do
|
11
|
+
|
12
|
+
end
|
13
|
+
|
14
|
+
describe "greater/less/equal expressions" do
|
15
|
+
it "handles numerics" do
|
16
|
+
test_expression "age is 23", {:age => 23}, true
|
17
|
+
test_expression "age is 23", {:age => 24}, false
|
18
|
+
|
19
|
+
test_expression "age is greater than 23", {:age => 24}, true
|
20
|
+
test_expression "age is greater than 23", {:age => 23}, false
|
21
|
+
|
22
|
+
test_expression "age is less than 23", {:age => 22}, true
|
23
|
+
test_expression "age is less than 23", {:age => 23}, false
|
24
|
+
|
25
|
+
test_expression "age is greater than or equal to 23", {:age => 23}, true
|
26
|
+
test_expression "age is greater than or equal to 23", {:age => 22}, false
|
27
|
+
|
28
|
+
test_expression "age is less than or equal to 23", {:age => 23}, true
|
29
|
+
test_expression "age is less than or equal to 23", {:age => 24}, false
|
30
|
+
end
|
31
|
+
|
32
|
+
it "handles strings" do
|
33
|
+
test_expression "name is samuel", {:name => "samuel"}, true
|
34
|
+
test_expression "name is samuel", {:name => "melchior"}, false
|
35
|
+
|
36
|
+
test_expression "name is greater than samuel", {:name => "simon"}, true
|
37
|
+
test_expression "name is greater than samuel", {:name => "samantha"}, false
|
38
|
+
|
39
|
+
test_expression "name is less than samuel", {:name => "lisa"}, true
|
40
|
+
test_expression "name is less than samuel", {:name => "samuel l jackson"}, false
|
41
|
+
|
42
|
+
test_expression "name is greater than or equal to samuel", {:name => "samuel"}, true
|
43
|
+
test_expression "name is greater than or equal to samuel", {:name => "samantha"}, false
|
44
|
+
|
45
|
+
test_expression "name is less than or equal to samuel", {:name => "samuel"}, true
|
46
|
+
test_expression "name is less than or equal to samuel", {:name => "samuel l jackson"}, false
|
47
|
+
end
|
48
|
+
|
49
|
+
it "handes dates" do
|
50
|
+
#TODO!!
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
describe "between expressions" do
|
55
|
+
it "handles numerics" do
|
56
|
+
test_expression "age is between 18 and 28", {:age => 23}, true
|
57
|
+
|
58
|
+
test_expression "age is between 18 and 28", {:age => 28}, true
|
59
|
+
test_expression "age is between 18 and 28", {:age => 18}, true
|
60
|
+
|
61
|
+
test_expression "age is between 18 and 28", {:age => 50}, false
|
62
|
+
test_expression "age is between 18 and 28", {:age => 17}, false
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
describe "matches expressions" do
|
67
|
+
it "handles strings" do
|
68
|
+
test_expression "name matches sam", {:name => "samuel"}, true
|
69
|
+
test_expression "name matches am", {:name => "samuel"}, true
|
70
|
+
test_expression "name matches uel", {:name => "samuel"}, true
|
71
|
+
|
72
|
+
test_expression "name matches xyz", {:name => "samuel"}, false
|
73
|
+
|
74
|
+
test_expression "name matches /^samuel$/", {:name => "samuel"}, true
|
75
|
+
test_expression "name matches /^samuel$/", {:name => "samuel "}, false
|
76
|
+
|
77
|
+
test_expression "name matches /daniel|jonas/", {:name => "daniel"}, true
|
78
|
+
test_expression "name matches /daniel|jonas/", {:name => "jonas"}, true
|
79
|
+
|
80
|
+
test_expression 'name matches /regex\/with escaping/', {:name => "regex/with escaping"}, true
|
81
|
+
test_expression "name matches /regex\\/with escaping/", {:name => "regex/with escaping"}, true
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
require 'ruva'
|
2
|
+
require 'ostruct'
|
3
|
+
|
4
|
+
describe Ruva do
|
5
|
+
describe "the 'if' syntax" do
|
6
|
+
|
7
|
+
before :all do
|
8
|
+
@person = OpenStruct.new
|
9
|
+
@person.age = 23
|
10
|
+
@person.city = "zurich"
|
11
|
+
@person.name = "samuel"
|
12
|
+
end
|
13
|
+
|
14
|
+
it "handles one liners" do
|
15
|
+
#person.if "age is 23 or higher" <-- TODO: support this
|
16
|
+
executed = false
|
17
|
+
@person.if "age is 23" do
|
18
|
+
executed = true
|
19
|
+
end
|
20
|
+
executed.should be true
|
21
|
+
|
22
|
+
executed = false
|
23
|
+
@person.if("age is 23") {
|
24
|
+
executed = true
|
25
|
+
}.else {
|
26
|
+
true.should be false
|
27
|
+
}
|
28
|
+
executed.should be true
|
29
|
+
|
30
|
+
executed = false
|
31
|
+
@person.if("age is 23") do
|
32
|
+
executed = true
|
33
|
+
end.else do
|
34
|
+
true.should be false
|
35
|
+
end
|
36
|
+
executed.should be true
|
37
|
+
|
38
|
+
executed = true
|
39
|
+
@person.if("age is 24") {
|
40
|
+
true.should be false
|
41
|
+
}.else {
|
42
|
+
executed = false
|
43
|
+
}
|
44
|
+
executed.should be false
|
45
|
+
end
|
46
|
+
|
47
|
+
it "handles multi line conditions" do
|
48
|
+
|
49
|
+
executed = false
|
50
|
+
@person.if "
|
51
|
+
age is 23
|
52
|
+
city is zurich
|
53
|
+
name is samuel
|
54
|
+
" do
|
55
|
+
executed = true
|
56
|
+
end
|
57
|
+
executed.should be true
|
58
|
+
|
59
|
+
executed = true
|
60
|
+
@person.if("
|
61
|
+
age is 24
|
62
|
+
city is zurich
|
63
|
+
name is samuel
|
64
|
+
") {
|
65
|
+
true.should be false
|
66
|
+
}.else {
|
67
|
+
executed = false
|
68
|
+
}
|
69
|
+
executed.should be false
|
70
|
+
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'ruva'
|
2
|
+
|
3
|
+
describe Ruva do
|
4
|
+
it "normalizes indentation" do
|
5
|
+
indented_text = "
|
6
|
+
this
|
7
|
+
text
|
8
|
+
is
|
9
|
+
indented
|
10
|
+
"
|
11
|
+
indented_text = Ruva.normalize_indentation indented_text
|
12
|
+
|
13
|
+
expected =
|
14
|
+
"this
|
15
|
+
text
|
16
|
+
is
|
17
|
+
indented
|
18
|
+
"
|
19
|
+
|
20
|
+
indented_text.should eq expected
|
21
|
+
end
|
22
|
+
end
|
data/spec/ruva_spec.rb
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'ruva'
|
2
|
+
require 'ostruct'
|
3
|
+
|
4
|
+
describe Ruva do
|
5
|
+
it "reads .ruva files" do
|
6
|
+
|
7
|
+
spec = Ruva.read "spec/conditions/simple_condition"
|
8
|
+
|
9
|
+
input = OpenStruct.new
|
10
|
+
input.age = 23
|
11
|
+
input.name = "samuel"
|
12
|
+
input.profession = "application engineer"
|
13
|
+
input.city = "zurich"
|
14
|
+
|
15
|
+
result = spec.evaluate(input)
|
16
|
+
|
17
|
+
result.satisfied.should be true
|
18
|
+
#puts result
|
19
|
+
end
|
20
|
+
end
|
metadata
ADDED
@@ -0,0 +1,123 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: ruva
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Samuel Mueller
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-07-21 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: citrus
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ~>
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 2.4.1
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ~>
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: 2.4.1
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: indentation-parser
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
type: :runtime
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: rspec
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ~>
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '2.6'
|
54
|
+
type: :development
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ~>
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '2.6'
|
62
|
+
description: Ruva is a simple utility to write conditions in a human readable manner.
|
63
|
+
email:
|
64
|
+
- mueller.samu@gmail.com
|
65
|
+
executables: []
|
66
|
+
extensions: []
|
67
|
+
extra_rdoc_files: []
|
68
|
+
files:
|
69
|
+
- .gitignore
|
70
|
+
- .travis.yml
|
71
|
+
- Gemfile
|
72
|
+
- LICENSE
|
73
|
+
- README.md
|
74
|
+
- Rakefile
|
75
|
+
- lib/ruva.rb
|
76
|
+
- lib/ruva/expression.rb
|
77
|
+
- lib/ruva/expression/equal_greater_less.rb
|
78
|
+
- lib/ruva/expression/expression_leaf_spec.rb
|
79
|
+
- lib/ruva/expression/identifier_comparable.rb
|
80
|
+
- lib/ruva/expression/is_between.rb
|
81
|
+
- lib/ruva/expression/matches.rb
|
82
|
+
- lib/ruva/expression/ruva_expression.citrus
|
83
|
+
- lib/ruva/specification.rb
|
84
|
+
- lib/ruva/specification/specification.rb
|
85
|
+
- lib/ruva/version.rb
|
86
|
+
- ruva.gemspec
|
87
|
+
- spec/conditions/simple_condition.ruva
|
88
|
+
- spec/expression_spec.rb
|
89
|
+
- spec/helpers/expression_helper.rb
|
90
|
+
- spec/inline_ruva_spec.rb
|
91
|
+
- spec/normalize_indentation_spec.rb
|
92
|
+
- spec/ruva_spec.rb
|
93
|
+
homepage: https://github.com/ssmm/ruva
|
94
|
+
licenses: []
|
95
|
+
post_install_message:
|
96
|
+
rdoc_options: []
|
97
|
+
require_paths:
|
98
|
+
- lib
|
99
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
100
|
+
none: false
|
101
|
+
requirements:
|
102
|
+
- - ! '>='
|
103
|
+
- !ruby/object:Gem::Version
|
104
|
+
version: '0'
|
105
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
106
|
+
none: false
|
107
|
+
requirements:
|
108
|
+
- - ! '>='
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
requirements: []
|
112
|
+
rubyforge_project:
|
113
|
+
rubygems_version: 1.8.24
|
114
|
+
signing_key:
|
115
|
+
specification_version: 3
|
116
|
+
summary: Ruva is a simple utility to write conditions in a human readable manner.
|
117
|
+
test_files:
|
118
|
+
- spec/conditions/simple_condition.ruva
|
119
|
+
- spec/expression_spec.rb
|
120
|
+
- spec/helpers/expression_helper.rb
|
121
|
+
- spec/inline_ruva_spec.rb
|
122
|
+
- spec/normalize_indentation_spec.rb
|
123
|
+
- spec/ruva_spec.rb
|