phenomenal 0.9.0
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.
- data/LICENSE +27 -0
- data/README +3 -0
- data/Rakefile +11 -0
- data/lib/phenomenal.rb +24 -0
- data/lib/phenomenal/adaptation.rb +79 -0
- data/lib/phenomenal/conflict_policies.rb +20 -0
- data/lib/phenomenal/context.rb +269 -0
- data/lib/phenomenal/dsl.rb +119 -0
- data/lib/phenomenal/feature.rb +8 -0
- data/lib/phenomenal/logger.rb +34 -0
- data/lib/phenomenal/manager.rb +317 -0
- data/lib/phenomenal/proc.rb +34 -0
- data/lib/phenomenal/relationships/context_relationships.rb +22 -0
- data/lib/phenomenal/relationships/dsl.rb +18 -0
- data/lib/phenomenal/relationships/feature_relationships.rb +42 -0
- data/lib/phenomenal/relationships/implication.rb +35 -0
- data/lib/phenomenal/relationships/relationship.rb +42 -0
- data/lib/phenomenal/relationships/relationships_manager.rb +63 -0
- data/lib/phenomenal/relationships/relationships_store.rb +73 -0
- data/lib/phenomenal/relationships/requirement.rb +26 -0
- data/lib/phenomenal/relationships/suggestion.rb +41 -0
- data/lib/phenomenal/version.rb +3 -0
- data/spec/adaptation_spec.rb +64 -0
- data/spec/behavior/adaptation_spec.rb +5 -0
- data/spec/behavior/combined_contexts_spec.rb +5 -0
- data/spec/behavior/composition_spec.rb +5 -0
- data/spec/behavior/conflict_policy_spec.rb +5 -0
- data/spec/behavior/open_context.rb +5 -0
- data/spec/behavior/relationships_spec.rb +249 -0
- data/spec/context_spec.rb +268 -0
- data/spec/dsl_spec.rb +181 -0
- data/spec/feature_spec.rb +5 -0
- data/spec/manager_spec.rb +84 -0
- data/spec/proc_spec.rb +20 -0
- data/spec/relationships/context_relationships_spec.rb +13 -0
- data/spec/relationships/dsl_spec.rb +13 -0
- data/spec/relationships/feature_relationships_spec.rb +13 -0
- data/spec/relationships/relationship_spec.rb +31 -0
- data/spec/relationships/relationships_manager_spec.rb +15 -0
- data/spec/relationships/relationships_store_spec.rb +19 -0
- data/spec/spec_helper.rb +18 -0
- data/spec/test_classes.rb +111 -0
- metadata +124 -0
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'logger'
|
2
|
+
require 'singleton'
|
3
|
+
|
4
|
+
class Phenomenal::Error < StandardError; end
|
5
|
+
|
6
|
+
class Phenomenal::Logger
|
7
|
+
attr_accessor :logger
|
8
|
+
include Singleton
|
9
|
+
|
10
|
+
def info(msg)
|
11
|
+
logger.info(msg)
|
12
|
+
end
|
13
|
+
|
14
|
+
def debug(msg)
|
15
|
+
logger.debug(msg)
|
16
|
+
end
|
17
|
+
|
18
|
+
def warn(msg)
|
19
|
+
logger.warn(msg)
|
20
|
+
end
|
21
|
+
|
22
|
+
def error(msg)
|
23
|
+
raise(Phenomenal::Error, msg)
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
def initialize
|
28
|
+
self.logger = Logger.new(STDOUT)
|
29
|
+
self.logger.level = Logger::DEBUG
|
30
|
+
self.logger.datetime_format = "%Y-%m-%d - %H:%M:%S"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
|
@@ -0,0 +1,317 @@
|
|
1
|
+
require 'singleton'
|
2
|
+
# This class manage the different contexts in the system and their interactions
|
3
|
+
class Phenomenal::Manager
|
4
|
+
include Singleton
|
5
|
+
include Phenomenal::ConflictPolicies
|
6
|
+
|
7
|
+
attr_accessor :active_adaptations, :deployed_adaptations, :contexts,
|
8
|
+
:default_context, :combined_contexts, :shared_contexts, :rmanager
|
9
|
+
|
10
|
+
# Register a new context
|
11
|
+
def register_context(context)
|
12
|
+
if context_defined?(context)
|
13
|
+
Phenomenal::Logger.instance.error(
|
14
|
+
"The context #{context} is already registered"
|
15
|
+
)
|
16
|
+
end
|
17
|
+
if context.name && context_defined?(context.name)
|
18
|
+
Phenomenal::Logger.instance.error(
|
19
|
+
"There is already a context with name: #{context.name}." +
|
20
|
+
" If you want to have named context it has to be a globally unique name"
|
21
|
+
)
|
22
|
+
end
|
23
|
+
contexts[context]=context
|
24
|
+
end
|
25
|
+
|
26
|
+
# Unregister a context (forget)
|
27
|
+
def unregister_context(context)
|
28
|
+
if context==default_context && !contexts.size==1
|
29
|
+
Phenomenal::Logger.instance.error(
|
30
|
+
"Default context can only be forgotten when alone"
|
31
|
+
)
|
32
|
+
else
|
33
|
+
contexts.delete(context)
|
34
|
+
|
35
|
+
# Forgot combined contexts
|
36
|
+
combined_contexts.delete(context)
|
37
|
+
if shared_contexts[context]
|
38
|
+
shared_contexts[context].each do |c|
|
39
|
+
c.forget
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# Restore default context
|
44
|
+
init_default() if context==default_context
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# Register a new adaptation for a registered context
|
49
|
+
def register_adaptation(adaptation)
|
50
|
+
default_adaptation = default_context.adaptations.find do|i|
|
51
|
+
i.concern?(adaptation.klass,adaptation.method_name,adaptation.instance_adaptation?)
|
52
|
+
end
|
53
|
+
if adaptation.context!=default_context && !default_adaptation
|
54
|
+
save_default_adaptation(adaptation.klass, adaptation.method_name,adaptation.instance_adaptation?)
|
55
|
+
end
|
56
|
+
activate_adaptation(adaptation) if adaptation.context.active?
|
57
|
+
end
|
58
|
+
|
59
|
+
# Unregister an adaptation for a registered context
|
60
|
+
def unregister_adaptation(adaptation)
|
61
|
+
deactivate_adaptation(adaptation) if adaptation.context.active?
|
62
|
+
end
|
63
|
+
|
64
|
+
# Activate the context 'context' and deploy the related adaptation
|
65
|
+
def activate_context(context)
|
66
|
+
begin
|
67
|
+
# Relationships managment
|
68
|
+
rmanager.activate_relationships(context) if context.just_activated?
|
69
|
+
|
70
|
+
# Activation of adaptations
|
71
|
+
context.adaptations.each{ |i| activate_adaptation(i) }
|
72
|
+
#puts "activation of #{context}"
|
73
|
+
if shared_contexts[context]
|
74
|
+
#puts "trigger activation of #{shared_contexts[context].first.information}"
|
75
|
+
shared_contexts[context].each do |combined_context|
|
76
|
+
need_activation=true
|
77
|
+
combined_contexts[combined_context].each do |shared_context|
|
78
|
+
need_activation=false if !shared_context.active?
|
79
|
+
end
|
80
|
+
combined_context.activate if need_activation
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
rescue Phenomenal::Error
|
85
|
+
context.deactivate # rollback the deployed adaptations
|
86
|
+
raise # throw up the exception
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
# Deactivate the adaptations (undeploy if needed)
|
91
|
+
def deactivate_context(context)
|
92
|
+
#Relationships managment
|
93
|
+
rmanager.deactivate_relationships(context)
|
94
|
+
#Adaptations deactivation
|
95
|
+
context.adaptations.each do |i|
|
96
|
+
deactivate_adaptation(i)
|
97
|
+
end
|
98
|
+
if shared_contexts[context]
|
99
|
+
shared_contexts[context].each do |combined_context|
|
100
|
+
while combined_context.active? do
|
101
|
+
combined_context.deactivate
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
# Call the old implementation of the method 'caller.caller_method'
|
108
|
+
def proceed(calling_stack,instance,*args,&block)
|
109
|
+
calling_adaptation = find_adapatation(calling_stack)
|
110
|
+
# IMPROVE Problems will appears if proceed called in a file where
|
111
|
+
# adaptations are defined but not in one of them=> how to check?
|
112
|
+
# IMPROVE Problems will also appears if two adaptations are defined on the same
|
113
|
+
# line using the ';' some check needed at add_adaptation ?
|
114
|
+
adaptations_stack = sorted_adaptations_for(calling_adaptation.klass,
|
115
|
+
calling_adaptation.method_name,calling_adaptation.instance_adaptation?)
|
116
|
+
calling_adaptation_index = adaptations_stack.find_index(calling_adaptation)
|
117
|
+
|
118
|
+
next_adaptation = adaptations_stack[calling_adaptation_index+1]
|
119
|
+
|
120
|
+
next_adaptation.bind(instance,*args, &block)
|
121
|
+
end
|
122
|
+
|
123
|
+
# Change the conflict resolution policy.
|
124
|
+
# These can be ones from the ConflictPolicies module or other ones
|
125
|
+
# Other one should return -1 or +1 following the resolution order
|
126
|
+
def change_conflict_policy (&block)
|
127
|
+
self.class.class_eval{define_method(:conflict_policy,&block)}
|
128
|
+
end
|
129
|
+
|
130
|
+
# Return the corresponding context or raise an error if the context isn't
|
131
|
+
# currently registered.
|
132
|
+
# The 'context' parameter can be either a reference to a context instance or
|
133
|
+
# a Symbol with the name of a named (not anonymous) context.
|
134
|
+
def find_context(context, *contexts)
|
135
|
+
if contexts.length==0
|
136
|
+
find_simple_context(context)
|
137
|
+
else #Combined contexts
|
138
|
+
contexts.insert(0,context)
|
139
|
+
find_combined_context(contexts)
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
# Check wether context 'context' exist in the context manager
|
144
|
+
# Context can be either the context name or the context instance itself
|
145
|
+
# Return the context if found, or nil otherwise
|
146
|
+
def context_defined?(context, *contexts)
|
147
|
+
c=nil
|
148
|
+
begin
|
149
|
+
c = find_context(context,*contexts)
|
150
|
+
rescue Phenomenal::Error
|
151
|
+
return nil
|
152
|
+
end
|
153
|
+
return c
|
154
|
+
end
|
155
|
+
# ==== Private methods ==== #
|
156
|
+
private
|
157
|
+
def find_simple_context(context)
|
158
|
+
find=nil
|
159
|
+
if !context.kind_of?(Phenomenal::Context)
|
160
|
+
a = contexts.find{|k,v| v.name==context}
|
161
|
+
if a
|
162
|
+
find = a[1]
|
163
|
+
end
|
164
|
+
else
|
165
|
+
find = context if contexts.has_key?(context)
|
166
|
+
end
|
167
|
+
if find
|
168
|
+
find
|
169
|
+
else
|
170
|
+
Phenomenal::Logger.instance.error(
|
171
|
+
"Unknown context #{context}"
|
172
|
+
)
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
def find_combined_context(contexts)
|
177
|
+
list=Array.new
|
178
|
+
contexts.each do |c|
|
179
|
+
# Use the object instance if already available
|
180
|
+
# otherwise use the symbol name
|
181
|
+
c = find_simple_context(c) if context_defined?(c)
|
182
|
+
if shared_contexts[c]==nil
|
183
|
+
list.clear
|
184
|
+
break
|
185
|
+
elsif list.length==0
|
186
|
+
# clone otherwise list.clear empty shared_contexts[c]
|
187
|
+
list=shared_contexts[c].clone
|
188
|
+
else
|
189
|
+
list=shared_contexts[c].find_all{|i| list.include?(i) }
|
190
|
+
end
|
191
|
+
end
|
192
|
+
if list.length==0
|
193
|
+
Phenomenal::Logger.instance.error(
|
194
|
+
"Unknown combined context #{contexts}"
|
195
|
+
)
|
196
|
+
elsif list.length==1
|
197
|
+
return list.first
|
198
|
+
else
|
199
|
+
Phenomenal::Logger.instance.error(
|
200
|
+
"Multiple definition of combined context #{contexts}"
|
201
|
+
)
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
# Activate the adaptation and redeploy the adaptations to take the new one
|
206
|
+
# one in account
|
207
|
+
def activate_adaptation(adaptation)
|
208
|
+
if !active_adaptations.include?(adaptation)
|
209
|
+
active_adaptations.push(adaptation)
|
210
|
+
end
|
211
|
+
redeploy_adaptation(adaptation.klass,adaptation.method_name,adaptation.instance_adaptation?)
|
212
|
+
end
|
213
|
+
|
214
|
+
# Deactivate the adaptation and redeploy the adaptations if necessary
|
215
|
+
def deactivate_adaptation(adaptation)
|
216
|
+
active_adaptations.delete(adaptation)
|
217
|
+
if deployed_adaptations.include?(adaptation)
|
218
|
+
deployed_adaptations.delete(adaptation)
|
219
|
+
redeploy_adaptation(adaptation.klass,adaptation.method_name,adaptation.instance_adaptation?)
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
# Redeploy the adaptations concerning klass.method_name according to the
|
224
|
+
# conflict policy
|
225
|
+
def redeploy_adaptation(klass, method_name,instance)
|
226
|
+
to_deploy = resolve_conflict(klass,method_name,instance)
|
227
|
+
# Do nothing when to_deploy==nil to break at default context deactivation
|
228
|
+
if !deployed_adaptations.include?(to_deploy) && to_deploy!=nil
|
229
|
+
deploy_adaptation(to_deploy)
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
# Deploy the adaptation
|
234
|
+
def deploy_adaptation(adaptation)
|
235
|
+
to_undeploy = deployed_adaptations.find do |i|
|
236
|
+
i.concern?(adaptation.klass,adaptation.method_name,adaptation.instance_adaptation?)
|
237
|
+
end
|
238
|
+
if to_undeploy!=adaptation # if new adaptation
|
239
|
+
deployed_adaptations.delete(to_undeploy)
|
240
|
+
deployed_adaptations.push(adaptation)
|
241
|
+
adaptation.deploy
|
242
|
+
end
|
243
|
+
end
|
244
|
+
|
245
|
+
# Save the default adaptation of a method, ie: the initial method
|
246
|
+
def save_default_adaptation(klass, method_name,instance)
|
247
|
+
if instance
|
248
|
+
method = klass.instance_method(method_name)
|
249
|
+
else
|
250
|
+
method = klass.method(method_name)
|
251
|
+
end
|
252
|
+
adaptation = default_context.add_adaptation(klass,method_name,instance,method)
|
253
|
+
end
|
254
|
+
|
255
|
+
# Return the adaptation that math the calling_stack, on the basis of the
|
256
|
+
# file and the line number --> proceed is always called under an
|
257
|
+
# adaptation definition
|
258
|
+
def find_adapatation(calling_stack)
|
259
|
+
source = calling_stack[0]
|
260
|
+
source_info = source.scan(/(.+\.rb):(\d+)/)[0]
|
261
|
+
call_file = source_info[0]
|
262
|
+
call_line = source_info[1].to_i
|
263
|
+
i = 0
|
264
|
+
match = nil
|
265
|
+
relevants = active_adaptations.select{ |i| i.src_file == call_file }
|
266
|
+
# Sort by src_line DESC
|
267
|
+
relevants.sort!{ |a,b| b.src_line <=> a.src_line }
|
268
|
+
relevants.each do |adaptation|
|
269
|
+
if adaptation.src_line <= call_line # Find first matching line
|
270
|
+
match = adaptation
|
271
|
+
break
|
272
|
+
end
|
273
|
+
end
|
274
|
+
|
275
|
+
if match==nil
|
276
|
+
Phenomenal::Logger.instance.error(
|
277
|
+
"Inexistant adaptation for proceed call at #{call_file}:#{call_line}"
|
278
|
+
)
|
279
|
+
end
|
280
|
+
match
|
281
|
+
end
|
282
|
+
|
283
|
+
# Return the best adaptation according to the resolution policy
|
284
|
+
def resolve_conflict(klass,method_name,instance)
|
285
|
+
sorted_adaptations_for(klass,method_name,instance).first
|
286
|
+
end
|
287
|
+
|
288
|
+
# Return the adaptations for a particular method sorted with the
|
289
|
+
# conflict policy
|
290
|
+
def sorted_adaptations_for(klass,method_name,instance)
|
291
|
+
relevant_adaptations =
|
292
|
+
active_adaptations.find_all { |i| i.concern?(klass, method_name,instance) }
|
293
|
+
relevant_adaptations.sort!{|a,b| conflict_policy(a,b)}
|
294
|
+
end
|
295
|
+
|
296
|
+
# Resolution policy
|
297
|
+
def conflict_policy(adaptation1, adaptation2)
|
298
|
+
age_conflict_policy(adaptation1, adaptation2)
|
299
|
+
end
|
300
|
+
|
301
|
+
# Set the default context
|
302
|
+
def init_default
|
303
|
+
self.default_context= Phenomenal::Feature.new(nil,self)
|
304
|
+
self.default_context.activate
|
305
|
+
end
|
306
|
+
|
307
|
+
# Private constructor because this is a singleton object
|
308
|
+
def initialize
|
309
|
+
@contexts = Hash.new
|
310
|
+
@deployed_adaptations = Array.new
|
311
|
+
@active_adaptations = Array.new
|
312
|
+
@combined_contexts = Hash.new
|
313
|
+
@shared_contexts = Hash.new
|
314
|
+
@rmanager = Phenomenal::RelationshipsManager.instance
|
315
|
+
init_default()
|
316
|
+
end
|
317
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# Add methods to Proc classe
|
2
|
+
class Proc
|
3
|
+
# Define a bind method on Proc object,
|
4
|
+
# This allow to execute a Proc as it was an instance method of reciever
|
5
|
+
# --> used for composition of instance methods adaptations
|
6
|
+
# Src: http://www.ruby-forum.com/topic/173699
|
7
|
+
def phenomenal_bind(receiver)
|
8
|
+
block, time = self, Time.now
|
9
|
+
(class << receiver; self end).class_eval do
|
10
|
+
method_name = "__bind_#{time.to_i}_#{time.usec}-#{rand(100000)}"
|
11
|
+
define_method(method_name, &block)
|
12
|
+
method = instance_method(method_name)
|
13
|
+
remove_method(method_name)
|
14
|
+
method
|
15
|
+
end.bind(receiver)
|
16
|
+
end
|
17
|
+
|
18
|
+
# Define a bind_class method on Proc object,
|
19
|
+
# This allow to execute a Proc as it was an class method of klass
|
20
|
+
# --> used for composition of class methods adaptations
|
21
|
+
def phenomenal_class_bind(klass)
|
22
|
+
block, time = self, Time.now
|
23
|
+
method_name = "__bind_#{time.to_i}_#{time.usec}-#{rand(100000)}"
|
24
|
+
method = nil
|
25
|
+
klass.instance_eval do
|
26
|
+
define_singleton_method(method_name, &block)
|
27
|
+
method = method(method_name)
|
28
|
+
(class << self; self end).instance_eval do
|
29
|
+
remove_method(method_name)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
method
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Phenomenal::ContextRelationships
|
2
|
+
def requires(context,*contexts)
|
3
|
+
contexts = contexts.push(context)
|
4
|
+
contexts.each do |target|
|
5
|
+
self.parent_feature.requirements_for(self,{:on=>target})
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
def implies(context,*contexts)
|
10
|
+
contexts = contexts.push(context)
|
11
|
+
contexts.each do |target|
|
12
|
+
self.parent_feature.implications_for(self,{:on=>target})
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def suggests(context,*contexts)
|
17
|
+
contexts = contexts.push(context)
|
18
|
+
contexts.each do |target|
|
19
|
+
self.parent_feature.suggestions_for(self,{:on=>target})
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Phenomenal::DSL
|
2
|
+
def self.define_relationships(klass)
|
3
|
+
klass.class_eval do
|
4
|
+
# Requirements
|
5
|
+
def requirements_for(source,targets)
|
6
|
+
Phenomenal::Manager.instance.default_context.requirements_for(source,targets)
|
7
|
+
end
|
8
|
+
# Implications
|
9
|
+
def implications_for(source,targets)
|
10
|
+
Phenomenal::Manager.instance.default_context.implications_for(source,targets)
|
11
|
+
end
|
12
|
+
# Suggestions
|
13
|
+
def suggestions_for(source,targets)
|
14
|
+
Phenomenal::Manager.instance.default_context.suggestions_for(source,targets)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module Phenomenal::FeatureRelationships
|
2
|
+
attr_accessor :relationships
|
3
|
+
|
4
|
+
def initialize_relationships
|
5
|
+
@relationships = Array.new
|
6
|
+
end
|
7
|
+
|
8
|
+
def requirements_for(source,targets)
|
9
|
+
add_relationship(source,targets,Phenomenal::Requirement)
|
10
|
+
end
|
11
|
+
|
12
|
+
def implications_for(source,targets)
|
13
|
+
add_relationship(source,targets,Phenomenal::Implication)
|
14
|
+
end
|
15
|
+
|
16
|
+
def suggestions_for(source,targets)
|
17
|
+
add_relationship(source,targets,Phenomenal::Suggestion)
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
def add_relationship(source,targets,type)
|
22
|
+
targets[:on]=Array.new.push(targets[:on]) if !targets[:on].is_a?(Array)
|
23
|
+
|
24
|
+
if targets[:on].nil?
|
25
|
+
Phenomenal::Logger.instance.error(
|
26
|
+
"Invalid relationship, missing target context"
|
27
|
+
)
|
28
|
+
end
|
29
|
+
targets[:on].each do |target|
|
30
|
+
r = type.new(source,target,self)
|
31
|
+
if relationships.find{|o| o==r}.nil?
|
32
|
+
relationships.push(r)
|
33
|
+
if self.active?
|
34
|
+
r.refresh
|
35
|
+
manager.rmanager.relationships.add(r)
|
36
|
+
r.activate_feature
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
end
|