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 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