ruler_coaster 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 80db58713844c8cb30db6f810310015911d49bfa
4
+ data.tar.gz: b740b937df58c288825fe409caf4e60f281dfb37
5
+ SHA512:
6
+ metadata.gz: decf550e775f6c84c184c03cbdc9b09739dd62dd297851d5b15b86b9b48a2e30913b0640b1a72962ecfac191e281044ff7c12200cc3767f388f76bd000a14c81
7
+ data.tar.gz: 77a6c4c91a9981ed7f1d849432c77de64986f41635933fa76d98886368520aa76d08fdb677ac8bb2b306c19a23268be09655d99c5a491b783d3c7620ddb2508d
@@ -0,0 +1,90 @@
1
+ # RulerCoaster
2
+ Rule engine
3
+
4
+ [![Code Climate](https://codeclimate.com/github/Streetbees/ruler/badges/gpa.svg)](https://codeclimate.com/github/Streetbees/ruler)
5
+ [![Test Coverage](https://codeclimate.com/github/Streetbees/ruler/badges/coverage.svg)](https://codeclimate.com/github/Streetbees/ruler/coverage)
6
+ [![Build Status](https://travis-ci.org/Streetbees/RulerCoaster.svg?branch=master)](https://travis-ci.org/Streetbees/ruler)
7
+ ## Usage
8
+
9
+ ```ruby
10
+ json_rule = {
11
+ type: "and",
12
+ left: {
13
+ type: "and",
14
+ left: {
15
+ type: "and",
16
+ left: {
17
+ type: "rule",
18
+ path: "user.gender",
19
+ operator: {
20
+ type: "equal",
21
+ value: "m"
22
+ }
23
+ },
24
+ right: {
25
+ type: "rule",
26
+ path: "user.location.city",
27
+ operator: {
28
+ type: "equal",
29
+ value: "London"
30
+ }
31
+ }
32
+ },
33
+ right: {
34
+ type: "rule",
35
+ path: "user.location.country",
36
+ operator: {
37
+ type: "equal",
38
+ value: "UK"
39
+ }
40
+ }
41
+ },
42
+ right: {
43
+ type: "rule",
44
+ path: "user.age",
45
+ operator: {
46
+ type: "equal",
47
+ value: 20
48
+ }
49
+ }
50
+ }
51
+ ```
52
+
53
+ ```ruby
54
+ data = {
55
+ user: {
56
+ gender: "m",
57
+ age: 20,
58
+ location: {
59
+ city: "London",
60
+ country: "UK"
61
+ }
62
+ }
63
+ }
64
+ ```
65
+
66
+ ```ruby
67
+ rules = RulerCoaster.parse(json_rule)
68
+
69
+ result = ruler.(data)
70
+
71
+ if result.success?
72
+ # continue
73
+ else
74
+ result.input # input from that failed
75
+ result.rule # first rule that failed
76
+ end
77
+ ```
78
+
79
+ ## Instalation
80
+
81
+ Add this to your Gemfile:
82
+ ```
83
+ gem 'ruler_coaster'
84
+ ```
85
+
86
+ And then execute:
87
+
88
+ ```
89
+ $> bundle install
90
+ ```
@@ -0,0 +1,13 @@
1
+ require 'ruler_coaster/parser'
2
+ require 'ruler_coaster/operator/base'
3
+ require 'ruler_coaster/logic/base'
4
+ require 'ruler_coaster/rule'
5
+ require 'ruler_coaster/result'
6
+ require 'ruler_coaster/no_result'
7
+ require 'ruler_coaster/attributes/base'
8
+
9
+ module RulerCoaster
10
+ class NavigationError < StandardError; end
11
+
12
+ extend Parser
13
+ end
@@ -0,0 +1,52 @@
1
+ require 'ruler_coaster/attributes/result'
2
+
3
+ module RulerCoaster
4
+ module Attributes
5
+ class Base
6
+ attr_reader :rules
7
+
8
+ def initialize(rules)
9
+ @rules = rules
10
+ end
11
+
12
+ def call(object)
13
+ errors = {}
14
+ blocked = []
15
+
16
+ each_attr_rule_result(object) do |attr, result|
17
+ if result.is_a?(RulerCoaster::NoResult)
18
+ blocked = block(blocked, attr)
19
+ elsif !result.success?
20
+ errors = hell(errors, attr, 'is invalid')
21
+ end
22
+ end
23
+
24
+ RulerCoaster::Attributes::Result.new(rules, object, errors, blocked)
25
+ end
26
+
27
+ private
28
+
29
+ def each_attr_rule_result(object)
30
+ rules.each do |attr, attr_rules|
31
+ attr_rules.each do |rule|
32
+ yield(attr, rule.call(object))
33
+ end
34
+ end
35
+ end
36
+
37
+ def hell(errors, attr, message)
38
+ attr_errors = errors[attr] ||= []
39
+
40
+ attr_errors << 'is invalid' unless attr_errors.include?(message)
41
+
42
+ errors[attr] = attr_errors
43
+ errors
44
+ end
45
+
46
+ def block(blocked, attr)
47
+ blocked << attr unless blocked.include?(attr)
48
+ blocked
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,21 @@
1
+ module RulerCoaster
2
+ module Attributes
3
+ class Result
4
+ attr_reader :rules,
5
+ :input,
6
+ :errors,
7
+ :blocked
8
+
9
+ def initialize(rules, input, errors, blocked = {})
10
+ @rules = rules
11
+ @input = input
12
+ @errors = errors
13
+ @blocked = blocked
14
+ end
15
+
16
+ def success?
17
+ @errors.keys.empty? && @blocked.empty?
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,15 @@
1
+ module RulerCoaster
2
+ module Logic
3
+ class And < Base
4
+ def call(object)
5
+ result = left.call(object)
6
+
7
+ if result.success?
8
+ right.call(object)
9
+ else
10
+ result
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,22 @@
1
+ require 'ruler_coaster/logic/mixin'
2
+
3
+ module RulerCoaster
4
+ module Logic
5
+ class Base
6
+ include Mixin
7
+
8
+ attr_reader :left,
9
+ :right
10
+
11
+ def initialize(left, right)
12
+ @left = left
13
+ @right = right
14
+ end
15
+ end
16
+ end
17
+ end
18
+
19
+ require 'ruler_coaster/logic/and'
20
+ require 'ruler_coaster/logic/or'
21
+ require 'ruler_coaster/logic/not'
22
+ require 'ruler_coaster/logic/nor'
@@ -0,0 +1,21 @@
1
+ module RulerCoaster
2
+ module Logic
3
+ module Mixin
4
+ def and(right)
5
+ Logic::And.new(self, right)
6
+ end
7
+
8
+ def or(right)
9
+ Logic::Or.new(self, right)
10
+ end
11
+
12
+ def not(right)
13
+ Logic::Not.new(self, right)
14
+ end
15
+
16
+ def nor(right)
17
+ Logic::Nor.new(self, right)
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,15 @@
1
+ module RulerCoaster
2
+ module Logic
3
+ class Nor < Base
4
+ def call(object)
5
+ result = left.call(object)
6
+
7
+ if !result.success?
8
+ right.call(object).negation
9
+ else
10
+ result.negation
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ module RulerCoaster
2
+ module Logic
3
+ class Not < Base
4
+ def call(object)
5
+ result = left.call(object)
6
+
7
+ if result.success?
8
+ right.call(object).negation
9
+ else
10
+ result.negation
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ module RulerCoaster
2
+ module Logic
3
+ class Or < Base
4
+ def call(object)
5
+ result = left.call(object)
6
+
7
+ if !result.success?
8
+ right.call(object)
9
+ else
10
+ result
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,14 @@
1
+ module RulerCoaster
2
+ class NoResult < Result
3
+ attr_reader :rule,
4
+ :input
5
+
6
+ def initialize(rule, input)
7
+ super(rule, input, false)
8
+ end
9
+
10
+ def negation
11
+ self
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,20 @@
1
+ module RulerCoaster
2
+ module Operator
3
+ class Base
4
+ attr_reader :assert_value
5
+
6
+ def initialize(assert_value)
7
+ @assert_value = assert_value
8
+ end
9
+ end
10
+ end
11
+ end
12
+
13
+ require 'ruler_coaster/operator/contain'
14
+ require 'ruler_coaster/operator/empty'
15
+ require 'ruler_coaster/operator/equal'
16
+ require 'ruler_coaster/operator/greater_than'
17
+ require 'ruler_coaster/operator/less_than'
18
+ require 'ruler_coaster/operator/not_contain'
19
+ require 'ruler_coaster/operator/not_empty'
20
+ require 'ruler_coaster/operator/not_equal'
@@ -0,0 +1,9 @@
1
+ module RulerCoaster
2
+ module Operator
3
+ class Contain < Base
4
+ def call(value)
5
+ assert_value.map(&:to_s).map(&:downcase).include?(value.to_s.downcase)
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,13 @@
1
+ module RulerCoaster
2
+ module Operator
3
+ class Empty < Base
4
+ def initialize(*_args)
5
+ super(nil)
6
+ end
7
+
8
+ def call(value)
9
+ value.to_s.empty?
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,9 @@
1
+ module RulerCoaster
2
+ module Operator
3
+ class Equal < Base
4
+ def call(value)
5
+ assert_value.casecmp(value.to_s) == 0
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ module RulerCoaster
2
+ module Operator
3
+ class GreaterThan < Base
4
+ def call(value)
5
+ value > assert_value
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ module RulerCoaster
2
+ module Operator
3
+ class LessThan < GreaterThan
4
+ def call(value)
5
+ !super
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ module RulerCoaster
2
+ module Operator
3
+ class NotContain < Contain
4
+ def call(value)
5
+ !super
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ module RulerCoaster
2
+ module Operator
3
+ class NotEmpty < Empty
4
+ def call(value)
5
+ !super(value)
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ module RulerCoaster
2
+ module Operator
3
+ class NotEqual < Equal
4
+ def call(value)
5
+ !super
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,78 @@
1
+ module RulerCoaster
2
+ module Parser
3
+ def parse(root)
4
+ visit(symbolyze_keys(root))
5
+ end
6
+
7
+ def visit(node)
8
+ send("visit_#{node[:type]}", node)
9
+ end
10
+
11
+ def visit_and(node)
12
+ Logic::And.new(visit(node[:left]), visit(node[:right]))
13
+ end
14
+
15
+ def visit_not(node)
16
+ Logic::Not.new(visit(node[:left]), visit(node[:right]))
17
+ end
18
+
19
+ def visit_or(node)
20
+ Logic::Or.new(visit(node[:left]), visit(node[:right]))
21
+ end
22
+
23
+ def visit_nor(node)
24
+ Logic::Nor.new(visit(node[:left]), visit(node[:right]))
25
+ end
26
+
27
+ def visit_rule(node)
28
+ Rule.new(node[:path], visit(node[:operator]))
29
+ end
30
+
31
+ def visit_equal(node)
32
+ Operator::Equal.new(node[:value])
33
+ end
34
+
35
+ def visit_not_equal(node)
36
+ Operator::NotEqual.new(node[:value])
37
+ end
38
+
39
+ def visit_greater_than(node)
40
+ Operator::GreaterThan.new(node[:value])
41
+ end
42
+
43
+ def visit_less_than(node)
44
+ Operator::LessThan.new(node[:value])
45
+ end
46
+
47
+ def visit_contain(node)
48
+ Operator::Contain.new(node[:value])
49
+ end
50
+
51
+ def visit_not_contain(node)
52
+ Operator::NotContain.new(node[:value])
53
+ end
54
+
55
+ def visit_empty(node)
56
+ Operator::Empty.new(node[:value])
57
+ end
58
+
59
+ def visit_not_empty(node)
60
+ Operator::NotEmpty.new(node[:value])
61
+ end
62
+
63
+ module_function
64
+
65
+ def symbolyze_keys(hash)
66
+ {}.tap do |symbolyzed_hash|
67
+ hash.each do |key, value|
68
+ symbolyzed_hash[key.to_sym] = \
69
+ if value.is_a?(Hash)
70
+ symbolyze_keys(value)
71
+ else
72
+ value
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,22 @@
1
+ module RulerCoaster
2
+ class Result
3
+ attr_reader :rule,
4
+ :input
5
+
6
+ def initialize(rule, input, success)
7
+ @rule = rule
8
+ @input = input
9
+ @success = success
10
+ end
11
+
12
+ def success?
13
+ @success == true
14
+ end
15
+
16
+ def negation
17
+ @success = !@success
18
+
19
+ self
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,41 @@
1
+ module RulerCoaster
2
+ class Rule
3
+ include Logic::Mixin
4
+
5
+ attr_reader :path,
6
+ :operator
7
+
8
+ def initialize(path, operator)
9
+ @path = path.split('.')
10
+ @operator = operator
11
+ end
12
+
13
+ def call(object)
14
+ input = navigate(object, path)
15
+
16
+ if input.nil? && !operator.is_a?(Operator::Empty)
17
+ NoResult.new(self, input)
18
+ else
19
+ Result.new(self, input, operator.call(input))
20
+ end
21
+ end
22
+
23
+ private
24
+
25
+ def navigate(object, navigation_path)
26
+ object = RulerCoaster::Parser.symbolyze_keys(object)
27
+
28
+ navigation_path = navigation_path.dup
29
+
30
+ next_key = navigation_path.shift.to_sym
31
+
32
+ if !navigation_path.empty?
33
+ navigate(object[next_key], navigation_path)
34
+ else
35
+ object[next_key]
36
+ end
37
+ rescue
38
+ raise NavigationError, "cannot navigate to #{path} on #{object.inspect}"
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,3 @@
1
+ module RulerCoaster
2
+ VERSION = '1.0.1'.freeze
3
+ end
metadata ADDED
@@ -0,0 +1,123 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ruler_coaster
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Streetbees Dev Team
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-08-02 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rubocop
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - '='
18
+ - !ruby/object:Gem::Version
19
+ version: 0.37.2
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - '='
25
+ - !ruby/object:Gem::Version
26
+ version: 0.37.2
27
+ - !ruby/object:Gem::Dependency
28
+ name: rspec
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '3.2'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '3.2'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: pry
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ description: RulerCoaster
70
+ email:
71
+ - dev@streetbees.com
72
+ executables: []
73
+ extensions: []
74
+ extra_rdoc_files: []
75
+ files:
76
+ - README.md
77
+ - lib/ruler_coaster.rb
78
+ - lib/ruler_coaster/attributes/base.rb
79
+ - lib/ruler_coaster/attributes/result.rb
80
+ - lib/ruler_coaster/logic/and.rb
81
+ - lib/ruler_coaster/logic/base.rb
82
+ - lib/ruler_coaster/logic/mixin.rb
83
+ - lib/ruler_coaster/logic/nor.rb
84
+ - lib/ruler_coaster/logic/not.rb
85
+ - lib/ruler_coaster/logic/or.rb
86
+ - lib/ruler_coaster/no_result.rb
87
+ - lib/ruler_coaster/operator/base.rb
88
+ - lib/ruler_coaster/operator/contain.rb
89
+ - lib/ruler_coaster/operator/empty.rb
90
+ - lib/ruler_coaster/operator/equal.rb
91
+ - lib/ruler_coaster/operator/greater_than.rb
92
+ - lib/ruler_coaster/operator/less_than.rb
93
+ - lib/ruler_coaster/operator/not_contain.rb
94
+ - lib/ruler_coaster/operator/not_empty.rb
95
+ - lib/ruler_coaster/operator/not_equal.rb
96
+ - lib/ruler_coaster/parser.rb
97
+ - lib/ruler_coaster/result.rb
98
+ - lib/ruler_coaster/rule.rb
99
+ - lib/ruler_coaster/version.rb
100
+ homepage: https://github.com/streetbees/rule-coaster
101
+ licenses: []
102
+ metadata: {}
103
+ post_install_message:
104
+ rdoc_options: []
105
+ require_paths:
106
+ - lib
107
+ required_ruby_version: !ruby/object:Gem::Requirement
108
+ requirements:
109
+ - - ">="
110
+ - !ruby/object:Gem::Version
111
+ version: '0'
112
+ required_rubygems_version: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - ">="
115
+ - !ruby/object:Gem::Version
116
+ version: '0'
117
+ requirements: []
118
+ rubyforge_project:
119
+ rubygems_version: 2.4.6
120
+ signing_key:
121
+ specification_version: 4
122
+ summary: Rule engine
123
+ test_files: []