noteikumi 0.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: 54301cbe45d5868cbfe3335a90a713e0b14d20c8
4
+ data.tar.gz: 72cd7208a75bd59dc401a627eedfe11d7b81c07b
5
+ SHA512:
6
+ metadata.gz: 947dd850767a14be17f9fa6c61a22ddd206a107fe5d9d5c4a0dcb47bb887b0d4d4fc8d0ba19b731c9f4a87009ae11d74c84e83c51492785f57b630858415f2c1
7
+ data.tar.gz: c26d4f9c130b0160172bb446ac12c3e5615243db59e1f574271ad33949d6fa8a36f8f08fa65d81f02ec9d83b2742d60d8fe730d6903698c5f2920abf394fa586
data/Gemfile ADDED
@@ -0,0 +1,11 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gem 'rspec', '~> 3.4.0'
4
+ gem 'mocha', '~> 1.1.0'
5
+ gem 'rake', '~> 10.5.0'
6
+ gem 'guard', '~> 2.13.0', :require => false
7
+ gem 'guard-shell',:require => false
8
+ gem 'guard-compat',:require => false
9
+ gem 'rubocop', :require => false
10
+ gem 'yard', :require => false
11
+ gem 'rubygems-tasks', :require => false
@@ -0,0 +1,37 @@
1
+ require "logger"
2
+
3
+ require "noteikumi/engine"
4
+ require "noteikumi/result"
5
+ require "noteikumi/rule"
6
+ require "noteikumi/rule_condition_validator"
7
+ require "noteikumi/rule_execution_scope"
8
+ require "noteikumi/rules"
9
+ require "noteikumi/state"
10
+ require "noteikumi/version"
11
+
12
+ # A light weight rule engine
13
+ #
14
+ # Visit https://github.com/ripienaar/noteikumi for more information
15
+ class Noteikumi
16
+ # Helper to create a new rule from a block
17
+ #
18
+ # @param rule_name [String,Symbol] unique name for this rule, Symbols preferred
19
+ # @param blk [Proc] the rule body with access to methods on {Rule}
20
+ # @return [Rule]
21
+ def self.rule(rule_name, &blk)
22
+ rule = Rule.new(rule_name)
23
+
24
+ rule.instance_eval(&blk)
25
+
26
+ rule
27
+ end
28
+
29
+ # Helper to create a new {Engine}
30
+ #
31
+ # @param path [String] a File::PATH_SEPARATOR seperated list of paths to load rules from
32
+ # @param logger [Logger] a logger to use
33
+ # @return [Engine]
34
+ def self.new_engine(path, logger)
35
+ Engine.new(path, logger)
36
+ end
37
+ end
@@ -0,0 +1,98 @@
1
+ class Noteikumi
2
+ # The main driver of the rule set
3
+ #
4
+ # @example create a engine and process some data
5
+ #
6
+ # engine = Engine.new("rules")
7
+ # state = engine.create_state
8
+ #
9
+ # state[:thing] = data_to_process
10
+ #
11
+ # engine.process_state(state)
12
+ #
13
+ # puts "%d rules ran" % [state.results.size]
14
+ class Engine
15
+ # The paths this engine consulted for rules
16
+ #
17
+ # @return [Array<String>] list of paths
18
+ attr_reader :path
19
+
20
+ # Creates an instance of the rule engine
21
+ #
22
+ # @param path [String] a File::PATH_SEPARATOR list of paths to load rules from
23
+ # @param logger [Logger]
24
+ # @return [Engine]
25
+ def initialize(path, logger=Logger.new(STDOUT))
26
+ @logger = logger
27
+ @path = parse_path(path)
28
+
29
+ rules_collection.load_rules
30
+ end
31
+
32
+ # Parse a File::PATH_SEPARATOR seperated path into expanded directories
33
+ #
34
+ # @api private
35
+ # @param path [String] The path to parse, should be a File::PATH_SEPARATOR list of paths
36
+ # @return [Array<String>]
37
+ def parse_path(path)
38
+ path.split(File::PATH_SEPARATOR).map do |part|
39
+ File.expand_path(part)
40
+ end
41
+ end
42
+
43
+ # Reset the run count on all loaded rules
44
+ #
45
+ # @api private
46
+ # @return [void]
47
+ def reset_rule_counts
48
+ rules_collection.rules.each(&:reset_counter)
49
+ end
50
+
51
+ # Given a state object process all the loaded rules
52
+ #
53
+ # @note the rule set is processed once only
54
+ # @param state [State]
55
+ # @return [Array<Result>]
56
+ def process_state(state)
57
+ raise("No rules have been loaded into engine %s" % self) if rules_collection.empty?
58
+
59
+ reset_rule_counts
60
+
61
+ rules_collection.by_priority.each do |rule|
62
+ state.process_rule(rule)
63
+ end
64
+
65
+ state.results
66
+ end
67
+
68
+ # Creates a new state that has an associated with this {Engine}
69
+ #
70
+ # @return [State]
71
+ def create_state
72
+ State.new(self, @logger)
73
+ end
74
+
75
+ # Iterates all the rules in the {Rules} collection
76
+ #
77
+ # @yieldparam rule [Rule]
78
+ # @return [void]
79
+ def each_rule
80
+ rules_collection.rules.each do |rule|
81
+ yield(rule)
82
+ end
83
+ end
84
+
85
+ # Creates and caches a rules collection
86
+ #
87
+ # @return [Rules]
88
+ def rules_collection
89
+ @rules ||= Rules.new(@path, @logger)
90
+ end
91
+
92
+ # :nodoc:
93
+ # @return [String]
94
+ def inspect
95
+ "#<%s:%s %d rules from %s>" % [self.class, object_id, rules_collection.count, @path.inspect]
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,88 @@
1
+ class Noteikumi
2
+ # Represents the result of running a specific rule
3
+ class Result
4
+ # The time it took for the rule to be processed
5
+ # @return [Float]
6
+ attr_reader :run_time
7
+
8
+ # The time the rule started
9
+ # @return [Time,nil]
10
+ attr_reader :start_time
11
+
12
+ # The time the rule ended
13
+ # @return [Time,nil]
14
+ attr_reader :end_time
15
+
16
+ # Any output produced by the rule
17
+ attr_reader :output
18
+
19
+ # The rule the result relates to
20
+ # @return [Rule,nil]
21
+ attr_reader :rule
22
+
23
+ # The exception a rule raised
24
+ # @return [Exception,nil]
25
+ attr_accessor :exception
26
+
27
+ # Any output returned from the rule run block
28
+ # @return [Object,nil]
29
+ attr_accessor :output
30
+
31
+ # Creates a result for a rule
32
+ #
33
+ # @param rule [Rule]
34
+ # @return [Result]
35
+ def initialize(rule)
36
+ @rule = rule
37
+ @start_time = nil
38
+ @end_time = nil
39
+ @run_time = nil
40
+ @exception = nil
41
+ @output = nil
42
+ @ran = false
43
+ end
44
+
45
+ # The rule name
46
+ #
47
+ # @return [String,Symbol]
48
+ def name
49
+ @rule.name
50
+ end
51
+
52
+ # If the result has an exception
53
+ #
54
+ # @return [Boolean]
55
+ def error?
56
+ !!exception
57
+ end
58
+
59
+ # Determines if this rule ran
60
+ #
61
+ # @return [Boolean]
62
+ def ran?
63
+ @ran
64
+ end
65
+
66
+ # Records the start time for the rule process
67
+ #
68
+ # @return [Time]
69
+ def start_processing
70
+ @ran = true
71
+ @start_time = Time.now
72
+ end
73
+
74
+ # Records that processing have ended
75
+ #
76
+ # @return [Time]
77
+ def stop_processing
78
+ @end_time = Time.now
79
+ @run_time = @end_time - @start_time
80
+ end
81
+
82
+ # :nodoc:
83
+ # @return [String]
84
+ def inspect
85
+ "#<%s:%s rule: %s ran: %s error: %s>" % [self.class, object_id, name, ran?, error?]
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,310 @@
1
+ class Noteikumi
2
+ # A class that represents an individual rule used by the engine
3
+ #
4
+ # Rules are generally stored in files named something_rule.rb in a rule
5
+ # directory, there are several samples of these in the examples dir
6
+ # and in docs on the wiki at GitHub
7
+ class Rule
8
+ # The priority for this fule
9
+ # @return [Fixnum]
10
+ attr_reader :priority
11
+
12
+ # The state this rule is being evaluated against
13
+ # @api private
14
+ # @return [State,nil]
15
+ attr_reader :state
16
+
17
+ # Named conditions for this rule
18
+ # @api private
19
+ # @see {condition}
20
+ # @return [Hash]
21
+ attr_reader :conditions
22
+
23
+ # Items the rule expect on the state
24
+ # @api private
25
+ # @see {requirement}
26
+ # @return [Array]
27
+ attr_reader :needs
28
+
29
+ # The concurrency safe level
30
+ # @api private
31
+ # @see {concurrency=}
32
+ # @return [:safe, :unsafe]
33
+ attr_reader :concurrent
34
+
35
+ # The run conditions for this rule
36
+ # @api private
37
+ # @see {run_when}
38
+ # @return [Proc]
39
+ attr_reader :run_condition
40
+
41
+ # The logic to run
42
+ # @api private
43
+ # @see {run}
44
+ # @return [Proc]
45
+ attr_reader :run_logic
46
+
47
+ # How many times this rule have been run
48
+ # @api private
49
+ # @return [Fixnum]
50
+ attr_reader :run_count
51
+
52
+ # The file the rule was found in
53
+ # @api private
54
+ # @return [String]
55
+ attr_accessor :file
56
+
57
+ # The rule name
58
+ # @api private
59
+ # @return [String,Symbol]
60
+ attr_accessor :name
61
+
62
+ # The logger used by this rule
63
+ # @api private
64
+ # @return [Logger]
65
+ attr_accessor :logger
66
+
67
+ # Creates a new rule
68
+ #
69
+ # @param name [String,Symbol] the name of the rule
70
+ # @return [Rule]
71
+ def initialize(name)
72
+ @name = name
73
+ @priority = 500
74
+ @concurrent = :unsafe
75
+ @needs = []
76
+ @conditions = {}
77
+ @state = nil
78
+ @file = "unknown file"
79
+ @run_count = 0
80
+
81
+ run_when { true }
82
+ run { raise("No execution logic provided for rule") }
83
+ end
84
+
85
+ # Checks if a condition matching the name has been created on the rule
86
+ #
87
+ # @see {#condition}
88
+ # @param condition [Symbol] condition name
89
+ # @return [Boolean]
90
+ def has_condition?(condition)
91
+ @conditions.include?(condition)
92
+ end
93
+
94
+ # Assign the provided state to the rule
95
+ #
96
+ # @param state [State] the state to store
97
+ # @return [void]
98
+ def assign_state(state)
99
+ @state = state
100
+ end
101
+
102
+ # Resets the state to nil state
103
+ #
104
+ # @return [void]
105
+ def reset_state
106
+ @state = nil
107
+ end
108
+
109
+ # Resets the run count for the rule to 0
110
+ #
111
+ # @return [void]
112
+ def reset_counter
113
+ @run_count = 0
114
+ end
115
+
116
+ # Assigns the state, yields to the block and resets it
117
+ #
118
+ # @param state [State] a state to act on
119
+ # @return [Object] the outcome from the block
120
+ def with_state(state)
121
+ assign_state(state)
122
+
123
+ yield
124
+ ensure
125
+ reset_state
126
+ end
127
+
128
+ # Runs the rule logic
129
+ #
130
+ # Rules are run within an instance of {RuleExecutionScope}
131
+ #
132
+ # @return [Result]
133
+ def run_rule_logic
134
+ @run_count += 1
135
+
136
+ result = new_result
137
+
138
+ begin
139
+ result.start_processing
140
+ result.output = RuleExecutionScope.new(self).run
141
+ rescue => e
142
+ logger.error("Error during processing of rule: %s: %s: %s" % [self, e.class, e.to_s])
143
+ logger.debug(e.backtrace.join("\n\t"))
144
+ result.exception = e
145
+ ensure
146
+ result.stop_processing
147
+ end
148
+
149
+ result
150
+ end
151
+
152
+ # Construct a result object for this rule
153
+ #
154
+ # @return [Result]
155
+ def new_result
156
+ Result.new(self)
157
+ end
158
+
159
+ # Process a rule after first checking all the requirements are met
160
+ #
161
+ # @param state [State] the state to use as scope
162
+ # @return [Result,nil] nil when the rule never ran due to state checks
163
+ def process(state)
164
+ result = nil
165
+
166
+ with_state(state) do
167
+ if state_meets_requirements?
168
+ if satisfies_run_condition?
169
+ logger.debug("Processing rule %s" % self)
170
+ result = run_rule_logic
171
+ else
172
+ logger.debug("Skipping processing rule due to run_when block returning false on %s" % self)
173
+ end
174
+ else
175
+ logger.debug("Skipping processing rule %s due to state check failing" % self)
176
+ end
177
+ end
178
+
179
+ result
180
+ end
181
+
182
+ # Determines if the run_when block is satisfied
183
+ #
184
+ # @return [Boolean]
185
+ def satisfies_run_condition?
186
+ validator = RuleConditionValidator.new(self)
187
+ validator.__should_run?
188
+ end
189
+
190
+ # Checks every requirement against the state
191
+ #
192
+ # @return [Boolean]
193
+ def state_meets_requirements?
194
+ @needs.each do |requirement|
195
+ valid, reason = state.meets_requirement?(requirement)
196
+
197
+ unless valid
198
+ logger.debug("State does not meet the requirements %s: %s" % [self, reason])
199
+ return false
200
+ end
201
+ end
202
+
203
+ true
204
+ end
205
+
206
+ # :nodoc:
207
+ # @return [String]
208
+ def to_s
209
+ "#<%s:%s run_count: %d priority: %d name: %s @ %s>" % [self.class, object_id, run_count, priority, name, file]
210
+ end
211
+
212
+ # Logic to execute once state has met to determine if the rule should be run
213
+ #
214
+ # @see #condition for an example
215
+ # @param blk [Proc] the checking logic that should return boolean
216
+ # @return [void]
217
+ def run_when(&blk)
218
+ raise("A block is needed to evaluate for run_when") unless block_given?
219
+ @run_condition = blk
220
+ end
221
+
222
+ # Creates the logic that will be run when all the conditions are met
223
+ #
224
+ # @see #requirement
225
+ # @see #condition
226
+ # @see #run_when
227
+ # @param blk [Proc] the logic to run
228
+ # @return [void]
229
+ def run(&blk)
230
+ raise("A block is needed to run") unless block_given?
231
+ @run_logic = blk
232
+ end
233
+
234
+ # Sets the rule priority
235
+ #
236
+ # @param priority [Fixnum]
237
+ # @return [Fixnum]
238
+ def rule_priority(priority)
239
+ @priority = Integer(priority)
240
+ end
241
+
242
+ # Sets the concurrency safe level
243
+ #
244
+ # This is mainly not used now but will result in the state becoming immutable
245
+ # when the level is :safe. This is with an eye on supporting parallel or threaded
246
+ # execution of rules in the long term
247
+ #
248
+ # @param level [:safe, :unsafe]
249
+ # @return [:safe, :unsafe]
250
+ def concurrency=(level)
251
+ raise("Concurrency has to be one of :safe, :unsafe") unless [:safe, :unsafe].include?(level)
252
+
253
+ @concurrent = level
254
+ end
255
+
256
+ # Determines if the concurrency level is :safe
257
+ #
258
+ # @return [Boolean]
259
+ def concurrent_safe?
260
+ @concurrent == :safe
261
+ end
262
+
263
+ # Creates a named condition
264
+ #
265
+ # @example create and use a condition
266
+ #
267
+ # condition(:weekend?) { Time.now.wday > 5 }
268
+ # condition(:daytime?) { Time.now.hour.between?(9, 18) }
269
+ #
270
+ # run_when { weekend? || !daytime? }
271
+ #
272
+ # @note these blocks must always return boolean and will be coerced to that
273
+ # @param name [Symbol] a unique name for this condition
274
+ # @param blk [Proc] the code to run when this condition is called
275
+ # @return [void]
276
+ def condition(name, &blk)
277
+ raise("Duplicate condition name %s" % name) if @conditions[name]
278
+ raise("A block is required for condition %s" % name) unless block_given?
279
+
280
+ @conditions[name] = blk
281
+
282
+ nil
283
+ end
284
+
285
+ # Sets a requirement that the state should meet
286
+ #
287
+ # @example require any scope item with a specific type
288
+ #
289
+ # requirement nil, String
290
+ #
291
+ # @example require that a specific item should be of a type
292
+ #
293
+ # requirement :thing, String
294
+ #
295
+ # @param args [Array] of requirements
296
+ # @return [void]
297
+ def requirement(*args)
298
+ case args.size
299
+ when 1
300
+ @needs << [nil, args[0]]
301
+ when 2
302
+ @needs << args
303
+ else
304
+ raise("Unsupported requirement input %s" % args.inspect)
305
+ end
306
+ end
307
+
308
+ nil
309
+ end
310
+ end
@@ -0,0 +1,85 @@
1
+ class Noteikumi
2
+ # This is a class used by {Rule#satisfies_run_condition?} to create a
3
+ # clean room to evaluate the state conditions in and provides helpers
4
+ # to expose named conditions as methods for use by {Rule#run_when}
5
+ #
6
+ # @api private
7
+ class RuleConditionValidator
8
+ # Creates a new validator
9
+ #
10
+ # @param rule [Rule]
11
+ # @return [RuleConditionValidator]
12
+ def initialize(rule)
13
+ @__rule = rule
14
+ end
15
+
16
+ # Checks if this is the first time the rule is being ran
17
+ #
18
+ # @return [Boolean]
19
+ def first_run?
20
+ @__rule.run_count == 0
21
+ end
22
+
23
+ # Checks if the state had any past failures
24
+ #
25
+ # @return [Boolean]
26
+ def state_had_failures?
27
+ @__rule.state.had_failures?
28
+ end
29
+
30
+ # Checks if a rule with a specific name acted on the state
31
+ #
32
+ # @param rule [Symbol,Rule]
33
+ # @return [Boolean]
34
+ def state_processed_by?(rule)
35
+ @__rule.state.processed_by?(rule)
36
+ end
37
+
38
+ # Runs the rules run condition
39
+ #
40
+ # @return [Boolean]
41
+ def __should_run?
42
+ instance_eval(&@__rule.run_condition)
43
+ end
44
+
45
+ # Determines if the rule has a condition by name
46
+ #
47
+ # @param condition [Symbol] the condition name
48
+ # @return [Boolean]
49
+ def __known_condition?(condition)
50
+ @__rule.has_condition?(condition)
51
+ end
52
+
53
+ # Retrieves a named condition from the rule
54
+ #
55
+ # @param condition [Symbol] the condition name
56
+ # @return [Proc,nil]
57
+ def __condition(condition)
58
+ @__rule.conditions[condition]
59
+ end
60
+
61
+ # Evaluate a named condition
62
+ #
63
+ # @param condition [Symbol] the condition name
64
+ # @param args [Array<Object>] arguments to pass to the condition
65
+ # @return [Boolean]
66
+ def __evaluate_condition(condition, *args)
67
+ result = !!__condition(condition).call(*args)
68
+ @__rule.logger.debug("Condition %s returned %s on %s" % [condition, result.inspect, @__rule])
69
+ result
70
+ end
71
+
72
+ # Provide method access to named based conditions
73
+ #
74
+ # @see {__evaluate_condition}
75
+ # @return [Boolean]
76
+ # @raise [NoMethodError] for unknown conditions
77
+ def method_missing(method, *args, &blk)
78
+ if __known_condition?(method)
79
+ __evaluate_condition(method, *args)
80
+ else
81
+ super
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,34 @@
1
+ class Noteikumi
2
+ # A class that exist to execute the logic of the rule
3
+ # @api private
4
+ class RuleExecutionScope
5
+ # The rule being ran
6
+ # @return [Rule]
7
+ attr_reader :rule
8
+
9
+ # The state the rule is processing
10
+ # @return [State]
11
+ attr_reader :state
12
+
13
+ # The active logger
14
+ # @return [Logger]
15
+ attr_reader :logger
16
+
17
+ # Creates a new scope object
18
+ #
19
+ # @param rule [Rule]
20
+ # @return [RuleExecutionScope
21
+ def initialize(rule)
22
+ @rule = rule
23
+ @state = rule.state
24
+ @logger = rule.logger
25
+ end
26
+
27
+ # Runs the rule logic within this scope
28
+ #
29
+ # @return [Object] the output from the rule
30
+ def run
31
+ @rule.run_logic.call
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,121 @@
1
+ class Noteikumi
2
+ # A collection of rules with various methods to load and find rules
3
+ class Rules
4
+ # The loaded rules
5
+ # @return [Array<Rule>]
6
+ attr_reader :rules
7
+
8
+ # The logger
9
+ # @api private
10
+ # @return [Logger]
11
+ attr_reader :logger
12
+
13
+ # Creates a rule collection
14
+ #
15
+ # @param rules_dir [Array<String>,String] a directory or list of directories to look for rules in
16
+ # @param logger [Logger] a logger to use
17
+ # @return Rules
18
+ def initialize(rules_dir, logger)
19
+ @rules = []
20
+ @logger = logger
21
+ @rules_dir = Array(rules_dir)
22
+ end
23
+
24
+ # Use a block to select rules out of the overall set
25
+ #
26
+ # @param blk [Proc] logic to use when selecting rules
27
+ # @return [Array<Rule>]
28
+ def select(&blk)
29
+ @rules.select(&blk)
30
+ end
31
+
32
+ # Return the rule ordered by priority
33
+ #
34
+ # @return [Array<Rule>]
35
+ def by_priority
36
+ @rules.sort_by(&:priority)
37
+ end
38
+
39
+ # The amont of rules loaded
40
+ #
41
+ # @return [Fixnum]
42
+ def size
43
+ @rules.size
44
+ end
45
+ alias_method :count, :size
46
+
47
+ # Determines if any rules are loaded
48
+ #
49
+ # @return [Boolean]
50
+ def empty?
51
+ @rules.empty?
52
+ end
53
+
54
+ # Append a rule to the collection
55
+ #
56
+ # @param rule [Rule]
57
+ # @return [Rule]
58
+ def <<(rule)
59
+ @rules << rule
60
+ end
61
+
62
+ # Get the names of all the rules
63
+ #
64
+ # @return [Array<String,Symbol>]
65
+ def rule_names
66
+ @rules.map(&:name)
67
+ end
68
+
69
+ # Load a rule from a file
70
+ #
71
+ # @param file [String] the file to load the rule from
72
+ # @return [Rule]
73
+ def load_rule(file)
74
+ raise("The rule %s is not readable" % file) unless File.readable?(file)
75
+
76
+ body = File.read(file)
77
+
78
+ clean = Object.new
79
+ rule = clean.instance_eval(body, file, 1)
80
+
81
+ rule.file = file
82
+ rule.logger = @logger
83
+
84
+ logger.debug("Loaded rule %s from %s" % [rule.name, file])
85
+
86
+ rule
87
+ end
88
+
89
+ # Load all the rules from the configured paths
90
+ #
91
+ # @return [void]
92
+ def load_rules
93
+ @rules_dir.each do |directory|
94
+ find_rules(directory).each do |rule|
95
+ rule = load_rule(File.join(directory, rule))
96
+
97
+ if rule_names.include?(rule.name)
98
+ raise("Already have a rule called %s, cannot load another" % rule.name)
99
+ end
100
+
101
+ self << rule
102
+ end
103
+ end
104
+ end
105
+
106
+ # Find all rules in a given directory
107
+ #
108
+ # Valid rules have names ending in _\_rule.rb_
109
+ #
110
+ # @param directory [String] the directory to look in
111
+ # @return [Array<String>] list of rule names
112
+ def find_rules(directory)
113
+ if File.directory?(directory)
114
+ Dir.entries(directory).grep(/_rule.rb$/)
115
+ else
116
+ @logger.debug("Could not find directory %s while looking for rules" % directory)
117
+ []
118
+ end
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,233 @@
1
+ class Noteikumi
2
+ # The state a rule will process, this is not a state machine
3
+ # but rather can be thought of like a scope
4
+ class State
5
+ # The list of result obtained from each rule
6
+ # @return [Array<Result>]
7
+ attr_reader :results
8
+
9
+ # A list of rules that acted on this state
10
+ # @return [Array<Rule>]
11
+ attr_reader :processed_by
12
+
13
+ # The engine this state is associated with
14
+ # @api private
15
+ # @return [Engine]
16
+ attr_reader :engine
17
+
18
+ # The logger used
19
+ # @api private
20
+ # @return [Logger]
21
+ attr_reader :logger
22
+
23
+ # Set the state mutable or not
24
+ # @return [Boolean]
25
+ attr_writer :mutable
26
+
27
+ # Creates a new state
28
+ #
29
+ # @param engine [Engine]
30
+ # @param logger [Logger]
31
+ # @return [State]
32
+ def initialize(engine, logger)
33
+ @items = {}
34
+ @results = []
35
+ @processed_by = []
36
+ @engine = engine
37
+ @logger = logger
38
+ @mutable = true
39
+ end
40
+
41
+ # Allow the state to be modified
42
+ #
43
+ # @return [void]
44
+ def allow_mutation
45
+ @mutable = true
46
+ end
47
+
48
+ # Prevent the state from being modified
49
+ #
50
+ # @return [void]
51
+ def prevent_mutation
52
+ @mutable = false
53
+ end
54
+
55
+ # Process a rule with the state
56
+ #
57
+ # @return [Result, nil] nil when the rule did not run
58
+ def process_rule(rule)
59
+ rule.concurrent_safe? ? prevent_mutation : allow_mutation
60
+
61
+ result = rule.process(self)
62
+
63
+ allow_mutation
64
+
65
+ record_rule(rule, result)
66
+ end
67
+
68
+ # Yields each recorded result
69
+ #
70
+ # @yieldparam result [Result] for every rule that ran
71
+ # @return [void]
72
+ def each_result
73
+ @results.each do |result|
74
+ yield(result)
75
+ end
76
+ end
77
+
78
+ # Adds a result to the list of results
79
+ #
80
+ # @param result [Result]
81
+ # @return [Result]
82
+ def add_result(result)
83
+ @results << result if result
84
+ end
85
+
86
+ # Checks all results for failures
87
+ #
88
+ # @return [Boolean] true when there were failures
89
+ def had_failures?
90
+ @results.map(&:error?).include?(true)
91
+ end
92
+
93
+ # Determines if a rule with a specific name acted on this state
94
+ #
95
+ # @param rule [Rule,Symbol] the rule name or a rule
96
+ # @return [Boolean]
97
+ def processed_by?(rule)
98
+ if rule.is_a?(Rule)
99
+ @processed_by.include?(rule)
100
+ else
101
+ @processed_by.map(&:name).include?(rule)
102
+ end
103
+ end
104
+
105
+ # Selects any item that has a certain ruby class type
106
+ #
107
+ # @param type [Class] the type to search for
108
+ # @return [Hash] items found
109
+ def items_by_type(type)
110
+ @items.select {|_, v| v.is_a?(type)} || []
111
+ end
112
+
113
+ # Checks if a given requirement is matched by this state
114
+ #
115
+ # @example
116
+ #
117
+ # state[:one] = "hello world"
118
+ #
119
+ # state.meets_requirements?(:one, String) => [true, "reason"]
120
+ # state.meets_requirements?(nil, String) => [true, "reason"]
121
+ # state.meets_requirements?(nil, Fixnum) => [false, "State has no items of class Fixnum"]
122
+ # state.meets_requirements?(:one, Fixnum) => [false, "State item :one is not a Fixnum"]
123
+ # state.meets_requirements?(:not_set, Fixnum) => [false, "State has no item not_set"]
124
+ #
125
+ # @param requirement [Array<key,type>]
126
+ # @return [Array<Boolean,String>]
127
+ def meets_requirement?(requirement)
128
+ key, klass = requirement
129
+
130
+ if key
131
+ return([false, "State has no item %s" % key]) unless include?(key)
132
+
133
+ unless self[key].is_a?(klass)
134
+ return [false, "State item %s is not a %s" % [key, klass]]
135
+ end
136
+ end
137
+
138
+ unless has_item_of_type?(klass)
139
+ return [false, "State has no items of class %s" % klass]
140
+ end
141
+
142
+ [true, "Valid state found"]
143
+ end
144
+
145
+ # Determines if any item in the State has a certain type
146
+ #
147
+ # @param type [Class] a ruby class to look for
148
+ # @return [Boolean]
149
+ def has_item_of_type?(type)
150
+ !items_by_type(type).empty?
151
+ end
152
+
153
+ # Determines if the state can be mutated
154
+ def mutable?
155
+ !!@mutable
156
+ end
157
+
158
+ # Records a rule having acted on this state
159
+ #
160
+ # If the result is not nil the actor will be record
161
+ # and the result stored, else it's a noop
162
+ #
163
+ # @param rule [Rule]
164
+ # @param result [Result]
165
+ # @return [void]
166
+ def record_rule(rule, result)
167
+ if result
168
+ @processed_by << rule
169
+ @results << result
170
+ end
171
+
172
+ result
173
+ end
174
+
175
+ # Checks if a item is in the state
176
+ #
177
+ # @param item [Symbol] item name
178
+ # @return [Boolean]
179
+ def include?(item)
180
+ @items.include?(item)
181
+ end
182
+ alias_method :has?, :include?
183
+
184
+ # sets the value of an item without checking if it's already set
185
+ #
186
+ # @param item [Symbol] the name of the item being stored
187
+ # @param value [Object] the item to store
188
+ # @return [Object] the item being stored
189
+ # @raise [StandardError] when the state is not mutable
190
+ def set(item, value)
191
+ raise("State is not mustable") unless mutable?
192
+
193
+ @items[item] = value
194
+ end
195
+ alias_method :[]=, :set
196
+
197
+ # Deletes an item
198
+ #
199
+ # @param item [Symbol] item to delete
200
+ # @raise [StandardError] when the state is not mutable
201
+ def delete(item)
202
+ raise("State is not mustable") unless mutable?
203
+
204
+ @items.delete(item)
205
+ end
206
+
207
+ # Adds an item
208
+ #
209
+ # See {#set} for a version of this that does not error if the
210
+ # item is already on the state
211
+ #
212
+ # @param item [Symbol] item to delete
213
+ # @param value [Object] item to store
214
+ # @return [Object] the value set
215
+ # @raise [StandardError] when the state is not mutable
216
+ # @raise [StandardError] when the item is already in the state
217
+ def add(item, value)
218
+ raise("State is not mustable") unless mutable?
219
+ raise("Already have item %s" % item) if has?(item)
220
+
221
+ set(item, value)
222
+ end
223
+
224
+ # Retrieves a item from the state
225
+ #
226
+ # @param item [Symbol] the item name
227
+ # @return [Object,nil] the value stored
228
+ def get(item)
229
+ @items[item]
230
+ end
231
+ alias_method :[], :get
232
+ end
233
+ end
@@ -0,0 +1,3 @@
1
+ class Noteikumi
2
+ VERSION = "0.0.1".freeze
3
+ end
metadata ADDED
@@ -0,0 +1,53 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: noteikumi
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - R.I.Pienaar
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-02-09 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: 'description: Light weight Rule Engine'
14
+ email: rip@devco.net
15
+ executables: []
16
+ extensions: []
17
+ extra_rdoc_files: []
18
+ files:
19
+ - lib/noteikumi.rb
20
+ - lib/noteikumi/rules.rb
21
+ - lib/noteikumi/engine.rb
22
+ - lib/noteikumi/rule_condition_validator.rb
23
+ - lib/noteikumi/rule_execution_scope.rb
24
+ - lib/noteikumi/result.rb
25
+ - lib/noteikumi/state.rb
26
+ - lib/noteikumi/version.rb
27
+ - lib/noteikumi/rule.rb
28
+ - Gemfile
29
+ homepage: http://devco.net/
30
+ licenses:
31
+ - Apache-2
32
+ metadata: {}
33
+ post_install_message:
34
+ rdoc_options: []
35
+ require_paths:
36
+ - lib
37
+ required_ruby_version: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - '>='
40
+ - !ruby/object:Gem::Version
41
+ version: '0'
42
+ required_rubygems_version: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - '>='
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ requirements: []
48
+ rubyforge_project:
49
+ rubygems_version: 2.0.14
50
+ signing_key:
51
+ specification_version: 4
52
+ summary: noteikumi
53
+ test_files: []