ruler_coaster 1.0.1

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.
@@ -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: []