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