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.
Files changed (43) hide show
  1. data/LICENSE +27 -0
  2. data/README +3 -0
  3. data/Rakefile +11 -0
  4. data/lib/phenomenal.rb +24 -0
  5. data/lib/phenomenal/adaptation.rb +79 -0
  6. data/lib/phenomenal/conflict_policies.rb +20 -0
  7. data/lib/phenomenal/context.rb +269 -0
  8. data/lib/phenomenal/dsl.rb +119 -0
  9. data/lib/phenomenal/feature.rb +8 -0
  10. data/lib/phenomenal/logger.rb +34 -0
  11. data/lib/phenomenal/manager.rb +317 -0
  12. data/lib/phenomenal/proc.rb +34 -0
  13. data/lib/phenomenal/relationships/context_relationships.rb +22 -0
  14. data/lib/phenomenal/relationships/dsl.rb +18 -0
  15. data/lib/phenomenal/relationships/feature_relationships.rb +42 -0
  16. data/lib/phenomenal/relationships/implication.rb +35 -0
  17. data/lib/phenomenal/relationships/relationship.rb +42 -0
  18. data/lib/phenomenal/relationships/relationships_manager.rb +63 -0
  19. data/lib/phenomenal/relationships/relationships_store.rb +73 -0
  20. data/lib/phenomenal/relationships/requirement.rb +26 -0
  21. data/lib/phenomenal/relationships/suggestion.rb +41 -0
  22. data/lib/phenomenal/version.rb +3 -0
  23. data/spec/adaptation_spec.rb +64 -0
  24. data/spec/behavior/adaptation_spec.rb +5 -0
  25. data/spec/behavior/combined_contexts_spec.rb +5 -0
  26. data/spec/behavior/composition_spec.rb +5 -0
  27. data/spec/behavior/conflict_policy_spec.rb +5 -0
  28. data/spec/behavior/open_context.rb +5 -0
  29. data/spec/behavior/relationships_spec.rb +249 -0
  30. data/spec/context_spec.rb +268 -0
  31. data/spec/dsl_spec.rb +181 -0
  32. data/spec/feature_spec.rb +5 -0
  33. data/spec/manager_spec.rb +84 -0
  34. data/spec/proc_spec.rb +20 -0
  35. data/spec/relationships/context_relationships_spec.rb +13 -0
  36. data/spec/relationships/dsl_spec.rb +13 -0
  37. data/spec/relationships/feature_relationships_spec.rb +13 -0
  38. data/spec/relationships/relationship_spec.rb +31 -0
  39. data/spec/relationships/relationships_manager_spec.rb +15 -0
  40. data/spec/relationships/relationships_store_spec.rb +19 -0
  41. data/spec/spec_helper.rb +18 -0
  42. data/spec/test_classes.rb +111 -0
  43. metadata +124 -0
@@ -0,0 +1,8 @@
1
+ class Phenomenal::Feature < Phenomenal::Context
2
+ include Phenomenal::FeatureRelationships
3
+
4
+ def initialize(name=nil,manager=nil)
5
+ super(name,manager)
6
+ initialize_relationships
7
+ end
8
+ end
@@ -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