rulezilla 0.1.4
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.
- checksums.yaml +7 -0
- data/.gitignore +1 -0
- data/.rspec +2 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +44 -0
- data/README.md +109 -0
- data/lib/rulezilla.rb +35 -0
- data/lib/rulezilla/basic_support.rb +11 -0
- data/lib/rulezilla/dsl.rb +120 -0
- data/lib/rulezilla/node.rb +44 -0
- data/lib/rulezilla/rule_builder.rb +101 -0
- data/lib/rulezilla/rule_builder/gherkin_to_condition_rule.rb +63 -0
- data/lib/rulezilla/rule_builder/gherkin_to_result_rule.rb +49 -0
- data/lib/rulezilla/tree.rb +76 -0
- data/lib/rulezilla/version.rb +3 -0
- data/rulezilla.gemspec +18 -0
- data/spec/features/default_support_methods.feature +21 -0
- data/spec/features/gherkin_dsl_framework.feature +90 -0
- data/spec/features/gherkin_rules/animal_rule.feature +16 -0
- data/spec/features/gherkin_rules/duration_rule.feature +14 -0
- data/spec/features/rulezilla_dsl_framwork.feature +299 -0
- data/spec/features/step_definitions/rule_steps.rb +64 -0
- data/spec/features/step_definitions/rulezilla_dsl_framework_steps.rb +119 -0
- data/spec/spec_helper.rb +21 -0
- metadata +82 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 0a5437383cf683007f42c90a096f3428103cdc46
|
4
|
+
data.tar.gz: 0e3366b7fee344c1d5c1958ee1f8ffd54a2b402b
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: abb77e8d80423a511e1ea78a0c4600bb103f628879268033e653573c5e264840aaacebe52512334597853991b9aee9e66b51ea49511c8cb4f7e2e65d58cd4271
|
7
|
+
data.tar.gz: 2c0100529a1032e5b02b12c522effc33d44cd8753cdfbd3541b9f1d83ab197e5864a098f44eab8238c9347f3657d1e0601d12dc2bf2ea17554b70f1d63fc034d
|
data/.gitignore
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
.bundle
|
data/.rspec
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
rulezilla (0.1.4)
|
5
|
+
gherkin
|
6
|
+
|
7
|
+
GEM
|
8
|
+
remote: https://rubygems.org/
|
9
|
+
specs:
|
10
|
+
coderay (1.1.0)
|
11
|
+
diff-lcs (1.2.5)
|
12
|
+
gherkin (2.12.2)
|
13
|
+
multi_json (~> 1.3)
|
14
|
+
method_source (0.8.2)
|
15
|
+
multi_json (1.10.1)
|
16
|
+
pry (0.9.12.6)
|
17
|
+
coderay (~> 1.0)
|
18
|
+
method_source (~> 0.8)
|
19
|
+
slop (~> 3.4)
|
20
|
+
rspec (3.0.0)
|
21
|
+
rspec-core (~> 3.0.0)
|
22
|
+
rspec-expectations (~> 3.0.0)
|
23
|
+
rspec-mocks (~> 3.0.0)
|
24
|
+
rspec-core (3.0.3)
|
25
|
+
rspec-support (~> 3.0.0)
|
26
|
+
rspec-expectations (3.0.3)
|
27
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
28
|
+
rspec-support (~> 3.0.0)
|
29
|
+
rspec-mocks (3.0.3)
|
30
|
+
rspec-support (~> 3.0.0)
|
31
|
+
rspec-support (3.0.3)
|
32
|
+
slop (3.5.0)
|
33
|
+
turnip (1.2.2)
|
34
|
+
gherkin (>= 2.5)
|
35
|
+
rspec (>= 2.0, < 4.0)
|
36
|
+
|
37
|
+
PLATFORMS
|
38
|
+
ruby
|
39
|
+
|
40
|
+
DEPENDENCIES
|
41
|
+
pry
|
42
|
+
rspec
|
43
|
+
rulezilla!
|
44
|
+
turnip
|
data/README.md
ADDED
@@ -0,0 +1,109 @@
|
|
1
|
+
[](https://semaphoreapp.com/simplybusiness/rulezilla)
|
2
|
+
[](https://codeclimate.com/repos/53ecc0416956800c1d01f6bf/feed)
|
3
|
+
|
4
|
+
|
5
|
+
rulezilla
|
6
|
+
=========
|
7
|
+
|
8
|
+
This provide a DSL to implement rules for various tasks. In the current version we are still rely user to have a certain level of Ruby knowledge to be able to use this DSL. The ultimate goal is for people without prior Ruby knowledge can change and even write the Rule.
|
9
|
+
|
10
|
+
|
11
|
+
# Installation
|
12
|
+
|
13
|
+
gem 'rulezilla', git: 'git@github.com:simplybusiness/rulezilla.git'
|
14
|
+
|
15
|
+
## Implementation
|
16
|
+
|
17
|
+
### Rules
|
18
|
+
|
19
|
+
#### Gherkin (Beta)
|
20
|
+
|
21
|
+
rulezilla Gherkin has only very limited support now
|
22
|
+
|
23
|
+
First set the path of which rulezilla can load the feature files from:
|
24
|
+
|
25
|
+
Rulezilla.gherkin_rules_path = 'absolute path'
|
26
|
+
|
27
|
+
The filename will then converted to the name of the class, e.g. `invalid_number_rule.feature` will generate `Rulezilla::InvalidNumberRule` class
|
28
|
+
|
29
|
+
We currently only support a very limited steps, please refer to:
|
30
|
+
|
31
|
+
[True / False](spec/features/gherkin_rules/animal_rule.feature)
|
32
|
+
|
33
|
+
[Duration](spec/features/gherkin_rules/duration_rule.feature)
|
34
|
+
|
35
|
+
|
36
|
+
#### Ruby
|
37
|
+
|
38
|
+
Please refer to the [feature](spec/features/rulezilla_dsl_framwork.feature) for further details
|
39
|
+
|
40
|
+
To use rulezilla, please include `Rulezilla::DSL` in your class:
|
41
|
+
|
42
|
+
class RoboticsRule
|
43
|
+
include Rulezilla::DSL
|
44
|
+
|
45
|
+
group :may_not_injure_human do
|
46
|
+
condition { not_injure_human? }
|
47
|
+
|
48
|
+
group :obey_human do
|
49
|
+
condition { do_as_human_told? }
|
50
|
+
|
51
|
+
define :protect_its_own_existence do
|
52
|
+
condition { protect_itself? }
|
53
|
+
result(true)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
default(false)
|
59
|
+
|
60
|
+
end
|
61
|
+
|
62
|
+
### Support Module
|
63
|
+
|
64
|
+
The support module will be automatically included if its name is `"#{rule_class_name}Support"`
|
65
|
+
|
66
|
+
e.g. if the rule class name is `RoboticsRule`, then the support would be `RoboticsRuleSupport`
|
67
|
+
|
68
|
+
module RoboticsRuleSupport
|
69
|
+
def protect_itself?
|
70
|
+
in_danger? && not_letting_itself_be_detroyed?
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
### How to execute the rule
|
75
|
+
|
76
|
+
if the entity is:
|
77
|
+
|
78
|
+
{
|
79
|
+
not_injure_human?: true,
|
80
|
+
do_as_human_told?: true,
|
81
|
+
in_danger?: true,
|
82
|
+
not_letting_itself_be_detroyed?: true
|
83
|
+
}
|
84
|
+
|
85
|
+
#### To get the first matching result
|
86
|
+
|
87
|
+
RoboticsRule.apply(entity) #=> true
|
88
|
+
|
89
|
+
#### To get all matching results
|
90
|
+
|
91
|
+
RoboticsRule.all(entity) #=> [true]
|
92
|
+
|
93
|
+
#### To get the trace of all node
|
94
|
+
|
95
|
+
RoboticsRule.trace(entity)
|
96
|
+
#=> all the nodes instance: [root, may_not_injure_human, obey_human, protect_its_own_existence] in sequence order.
|
97
|
+
|
98
|
+
#### To get all results from the Rule
|
99
|
+
|
100
|
+
RoboticsRule.results #=> [true, false]
|
101
|
+
|
102
|
+
|
103
|
+
# Syntax
|
104
|
+
|
105
|
+
Please refer to the features for DSL syntax:
|
106
|
+
|
107
|
+
[DSL Feature](spec/features/rulezilla_dsl_framwork.feature),
|
108
|
+
|
109
|
+
[Default Support Methods Feature](spec/features/default_support_methods.feature)
|
data/lib/rulezilla.rb
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'rulezilla/node'
|
2
|
+
require 'rulezilla/tree'
|
3
|
+
require 'rulezilla/basic_support'
|
4
|
+
require 'rulezilla/dsl'
|
5
|
+
require 'rulezilla/rule_builder'
|
6
|
+
|
7
|
+
module Rulezilla
|
8
|
+
extend self
|
9
|
+
|
10
|
+
attr_accessor :gherkin_rules_path
|
11
|
+
|
12
|
+
def const_missing(name)
|
13
|
+
raise 'Missing Gherkin Rule Path' if gherkin_rules_path.nil?
|
14
|
+
|
15
|
+
matching_file = Dir.glob(File.join(gherkin_rules_path, '**', '*')).detect do |file|
|
16
|
+
File.basename(file, ".*") == underscore(name.to_s)
|
17
|
+
end
|
18
|
+
|
19
|
+
if matching_file.nil?
|
20
|
+
super
|
21
|
+
else
|
22
|
+
Rulezilla::RuleBuilder.from_file(name, matching_file).build
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
def underscore(camel_string)
|
28
|
+
camel_string.gsub(/::/, '/').
|
29
|
+
gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
|
30
|
+
gsub(/([a-z\d])([A-Z])/,'\1_\2').
|
31
|
+
tr("-", "_").
|
32
|
+
downcase
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
@@ -0,0 +1,120 @@
|
|
1
|
+
module Rulezilla
|
2
|
+
module DSL
|
3
|
+
def self.included(base)
|
4
|
+
base.extend ClassMethods
|
5
|
+
create_klass(base)
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.get_super(klass)
|
9
|
+
Object.const_get (['Object'] + klass.name.split('::'))[0..-2].join('::')
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.demodulize_klass_name(klass_name)
|
13
|
+
klass_name.split('::').last
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.create_klass(parent_klass)
|
17
|
+
klass_name = parent_klass.name
|
18
|
+
|
19
|
+
klass = get_super(parent_klass).const_set("#{demodulize_klass_name(klass_name)}Record", Class.new)
|
20
|
+
|
21
|
+
klass.class_eval do
|
22
|
+
include Rulezilla::BasicSupport
|
23
|
+
include Object.const_get("#{klass_name}Support") rescue NameError
|
24
|
+
|
25
|
+
attr_reader :record
|
26
|
+
|
27
|
+
define_method(:initialize) do |record|
|
28
|
+
record = OpenStruct.new(record) if record.is_a?(Hash)
|
29
|
+
instance_variable_set('@record', record)
|
30
|
+
end
|
31
|
+
|
32
|
+
define_method(:method_missing) do |meth, *args, &block|
|
33
|
+
record.send(meth, *args, &block)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
private_class_method :create_klass, :get_super, :demodulize_klass_name
|
38
|
+
end
|
39
|
+
|
40
|
+
module ClassMethods
|
41
|
+
def mandatory_attributes
|
42
|
+
@mandatory_attributes ||= []
|
43
|
+
end
|
44
|
+
|
45
|
+
def apply(record={})
|
46
|
+
result_node = trace(record).last
|
47
|
+
|
48
|
+
result_node.nil? ? nil : result_node.result(record_klass_instance(record))
|
49
|
+
end
|
50
|
+
|
51
|
+
def all(record={})
|
52
|
+
validate_missing_attributes(record)
|
53
|
+
result_node = tree.find_all(record_klass_instance(record))
|
54
|
+
|
55
|
+
result_node.nil? ? nil : result_node.map { |node| node.result(record_klass_instance(record)) }
|
56
|
+
end
|
57
|
+
|
58
|
+
def results(record=nil)
|
59
|
+
tree.all_results(record_klass_instance(record)).uniq
|
60
|
+
end
|
61
|
+
|
62
|
+
def trace(record=nil)
|
63
|
+
validate_missing_attributes(record)
|
64
|
+
|
65
|
+
tree.trace(record_klass_instance(record))
|
66
|
+
end
|
67
|
+
|
68
|
+
def include_rule(rule)
|
69
|
+
if rule.ancestors.include?(Rulezilla::DSL)
|
70
|
+
tree.clone_and_append_children(rule.tree.root_node.children)
|
71
|
+
else
|
72
|
+
raise "#{rule.name} is not a Rulezilla class"
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def tree
|
77
|
+
@tree ||= Tree.new(Node.new())
|
78
|
+
end
|
79
|
+
|
80
|
+
private
|
81
|
+
|
82
|
+
def record_klass_instance(record)
|
83
|
+
Object.const_get("#{self.name}Record").new(record)
|
84
|
+
end
|
85
|
+
|
86
|
+
def missing_attributes(record)
|
87
|
+
record = OpenStruct.new(record) if record.is_a?(Hash)
|
88
|
+
mandatory_attributes.map(&:to_sym) - record.methods
|
89
|
+
end
|
90
|
+
|
91
|
+
def validate_missing_attributes(record)
|
92
|
+
raise "Missing #{missing_attributes(record).join(', ')} attributes from: #{record}" unless missing_attributes(record).empty?
|
93
|
+
end
|
94
|
+
|
95
|
+
# DSL methods
|
96
|
+
def validate_attributes_presence(*fields)
|
97
|
+
@mandatory_attributes = mandatory_attributes | fields
|
98
|
+
end
|
99
|
+
|
100
|
+
def define(name=nil, &block)
|
101
|
+
tree.create_and_move_to_child(name)
|
102
|
+
|
103
|
+
instance_eval(&block)
|
104
|
+
tree.go_up
|
105
|
+
end
|
106
|
+
alias_method :group, :define
|
107
|
+
|
108
|
+
def condition(&block)
|
109
|
+
tree.current_node.condition = block
|
110
|
+
end
|
111
|
+
|
112
|
+
def result(value=nil, &block)
|
113
|
+
tree.current_node.result = value.nil? ? block : value
|
114
|
+
end
|
115
|
+
alias_method :default, :result
|
116
|
+
|
117
|
+
# End of DSL methods
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module Rulezilla
|
2
|
+
class Node
|
3
|
+
attr_accessor :parent, :children
|
4
|
+
attr_reader :condition, :default, :name
|
5
|
+
|
6
|
+
def initialize
|
7
|
+
@children = []
|
8
|
+
end
|
9
|
+
|
10
|
+
def has_children?
|
11
|
+
children.any?
|
12
|
+
end
|
13
|
+
|
14
|
+
def has_result?
|
15
|
+
!@result.nil?
|
16
|
+
end
|
17
|
+
|
18
|
+
def applies?(record)
|
19
|
+
return true if condition.nil?
|
20
|
+
record.instance_eval(&condition)
|
21
|
+
end
|
22
|
+
|
23
|
+
def result(record)
|
24
|
+
@result.is_a?(Proc) ? record.instance_eval(&@result) : @result
|
25
|
+
end
|
26
|
+
|
27
|
+
def condition=(block)
|
28
|
+
@condition = block
|
29
|
+
end
|
30
|
+
|
31
|
+
def name=(value)
|
32
|
+
@name = value.to_s
|
33
|
+
end
|
34
|
+
|
35
|
+
def result=(block)
|
36
|
+
@result = block
|
37
|
+
end
|
38
|
+
|
39
|
+
def add_child(node)
|
40
|
+
node.parent = self
|
41
|
+
children << node
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
require 'gherkin/parser/parser'
|
2
|
+
require 'gherkin/formatter/json_formatter'
|
3
|
+
require 'stringio'
|
4
|
+
require 'json'
|
5
|
+
require 'rulezilla/rule_builder/gherkin_to_result_rule'
|
6
|
+
require 'rulezilla/rule_builder/gherkin_to_condition_rule'
|
7
|
+
|
8
|
+
module Rulezilla
|
9
|
+
class RuleBuilder
|
10
|
+
|
11
|
+
def self.from_file(name, file)
|
12
|
+
new(name, IO.read(file))
|
13
|
+
end
|
14
|
+
|
15
|
+
attr_reader :name, :content
|
16
|
+
|
17
|
+
def initialize(name, content)
|
18
|
+
@name = name
|
19
|
+
@content = content
|
20
|
+
end
|
21
|
+
|
22
|
+
def build
|
23
|
+
klass_definition = rules.map do |rule|
|
24
|
+
rule = RuleDefinition.new(rule, step_keyword)
|
25
|
+
|
26
|
+
condition_definition = rule.conditions.empty? ? "" : "condition { #{rule.conditions} }"
|
27
|
+
|
28
|
+
"""
|
29
|
+
define \"#{rule.name}\" do
|
30
|
+
#{condition_definition}
|
31
|
+
result(#{rule.result})
|
32
|
+
end
|
33
|
+
"""
|
34
|
+
end.join("\n")
|
35
|
+
|
36
|
+
klass = Rulezilla.const_set(name, Class.new)
|
37
|
+
|
38
|
+
klass.class_eval('include Rulezilla::DSL')
|
39
|
+
klass.class_eval(klass_definition)
|
40
|
+
|
41
|
+
klass
|
42
|
+
end
|
43
|
+
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def step_keyword
|
48
|
+
gherkin_json.first['name'].gsub(/\s?rule/i, '')
|
49
|
+
end
|
50
|
+
|
51
|
+
def rules
|
52
|
+
gherkin_json.first['elements']
|
53
|
+
end
|
54
|
+
|
55
|
+
def gherkin_json
|
56
|
+
@gherkin_json ||= begin
|
57
|
+
io = StringIO.new
|
58
|
+
formatter = Gherkin::Formatter::JSONFormatter.new(io)
|
59
|
+
parser = Gherkin::Parser::Parser.new(formatter)
|
60
|
+
|
61
|
+
parser.parse(content, content, 0)
|
62
|
+
formatter.done
|
63
|
+
|
64
|
+
JSON.parse(io.string)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
|
69
|
+
class RuleDefinition
|
70
|
+
def initialize(gherkin_json, step_keyword)
|
71
|
+
@gherkin_json = gherkin_json
|
72
|
+
@step_keyword = step_keyword
|
73
|
+
end
|
74
|
+
|
75
|
+
def name
|
76
|
+
@gherkin_json['name']
|
77
|
+
end
|
78
|
+
|
79
|
+
def conditions
|
80
|
+
condition_steps = steps.reject{|step| step['keyword'].strip.downcase == 'then'}
|
81
|
+
conditions = condition_steps.map do |step|
|
82
|
+
::Rulezilla::RuleBuilder::GherkinToConditionRule.apply(record(step))
|
83
|
+
end.reject{|condition| condition == Rulezilla::RuleBuilder::DefaultCondition}.join(' && ')
|
84
|
+
end
|
85
|
+
|
86
|
+
def result
|
87
|
+
::Rulezilla::RuleBuilder::GherkinToResultRule.apply record(steps.detect{|step| step['keyword'].strip.downcase == 'then'})
|
88
|
+
end
|
89
|
+
|
90
|
+
private
|
91
|
+
|
92
|
+
def steps
|
93
|
+
@steps ||= @gherkin_json['steps']
|
94
|
+
end
|
95
|
+
|
96
|
+
def record(step)
|
97
|
+
step.merge(step_keyword: @step_keyword)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|