rulezilla 0.1.4

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,2 @@
1
+ --colour --order rand
2
+ -r turnip/rspec
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source 'https://rubygems.org'
2
+ gemspec
3
+
4
+ gem 'rspec'
5
+ gem 'turnip'
6
+ gem 'pry'
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
+ [![Semaphore](https://semaphoreapp.com/api/v1/projects/e488365d-9c57-4431-916a-72aea091d1b5/229083/shields_badge.png)](https://semaphoreapp.com/simplybusiness/rulezilla)
2
+ [![Code Climate](https://codeclimate.com/repos/53ecc0416956800c1d01f6bf/badges/76b47eaeffc33e312508/gpa.svg)](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,11 @@
1
+ module Rulezilla
2
+ module BasicSupport
3
+
4
+ def does_not?(value)
5
+ value == false
6
+ end
7
+
8
+ alias_method :is_not?, :does_not?
9
+
10
+ end
11
+ 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