forked from ManageIQ/manageiq
-
Notifications
You must be signed in to change notification settings - Fork 0
/
supports_feature_mixin.rb
266 lines (243 loc) · 10.6 KB
/
supports_feature_mixin.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
module SupportsFeatureMixin
#
# Including this in a model gives you a DSL to make features supported or not
#
# class Post
# include SupportsFeatureMixin
# supports :publish
# supports_not :fake, :reason => 'We keep it real'
# supports :archive do
# unsupported_reason_add(:archive, 'Its too good') if featured?
# end
# end
#
# To make a feature conditionally supported, pass a block to the +supports+ method.
# The block is evaluated in the context of the instance.
# If you call the private method +unsupported_reason_add+ with the feature
# and a reason, then the feature will be unsupported and the reason will be
# accessible through
#
# instance.unsupported_reason(:feature)
#
# The above allows you to call +supports_feature?+ or +supports?(feature) :methods
# on the Class and Instance
#
# Post.supports_publish? # => true
# Post.supports?(:publish) # => true
# Post.new.supports_publish? # => true
# Post.supports_fake? # => false
# Post.supports_archive? # => true
# Post.new(featured: true).supports_archive? # => false
#
# To get a reason why a feature is unsupported use the +unsupported_reason+ method
#
# Post.unsupported_reason(:publish) # => "Feature not supported"
# Post.unsupported_reason(:fake) # => "We keep it real"
# Post.new(featured: true).unsupported_reason(:archive) # => "Its too good"
#
# To query for known features you can ask the class or the instance via +feature_known?+
#
# Post.feature_known?('fake') # => true
# Post.new.feature_known?(:fake) # => true
# Post.new.feature_known?(:alert) # => false
#
# If you include this concern in a Module that gets included by the Model
# you have to extend that model with +ActiveSupport::Concern+ and wrap the
# +supports+ calls in an +included+ block. This is also true for modules in between!
#
# module Operations
# extend ActiveSupport::Concern
# module Power
# extend ActiveSupport::Concern
# included do
# supports :operation
# end
# end
# end
#
extend ActiveSupport::Concern
QUERYABLE_FEATURES = {
:add_host => 'Add Host',
:add_interface => 'Add Interface',
:associate_floating_ip => 'Associate a Floating IP',
:clone => 'Clone',
# FIXME: this is just a internal helper and should be refactored
:control => 'Basic control operations',
:cloud_tenant_mapping => 'CloudTenant mapping',
:cloud_object_store_container_create => 'Create Object Store Container',
:cloud_object_store_container_clear => 'Clear Object Store Container',
:create => 'Creation',
:backup_create => 'CloudVolume backup creation',
:backup_restore => 'CloudVolume backup restore',
:cinder_service => 'Cinder storage service',
:create_floating_ip => 'Floating IP Creation',
:create_host_aggregate => 'Host Aggregate Creation',
:create_network_router => 'Network Router Creation',
:create_security_group => 'Security Group Creation',
:console => 'Remote Console',
:external_logging => 'Launch External Logging UI',
:swift_service => 'Swift storage service',
:delete => 'Deletion',
:delete_aggregate => 'Host Aggregate Deletion',
:delete_floating_ip => 'Floating IP Deletion',
:delete_network_router => 'Network Router Deletion',
:delete_security_group => 'Security Group Deletion',
:disassociate_floating_ip => 'Disassociate a Floating IP',
:discovery => 'Discovery of Managers for a Provider',
:evacuate => 'Evacuation',
:launch_cockpit => 'Launch Cockpit UI',
:live_migrate => 'Live Migration',
:migrate => 'Migration',
:capture => 'Capture of Capacity & Utilization Metrics',
:provisioning => 'Provisioning',
:publish => 'Publishing',
:quick_stats => 'Quick Stats',
:reboot_guest => 'Reboot Guest Operation',
:reconfigure => 'Reconfiguration',
:reconfigure_disks => 'Reconfigure Virtual Machines Disks',
:refresh_network_interfaces => 'Refresh Network Interfaces for a Host',
:refresh_new_target => 'Refresh non-existing record',
:regions => 'Regions of a Provider',
:remove_all_snapshots => 'Remove all snapshots',
:remove_host => 'Remove Host',
:remove_interface => 'Remove Interface',
:remove_snapshot => 'Remove Snapshot',
:remove_snapshot_by_description => 'Remove snapshot having a description',
:reset => 'Reset',
:resize => 'Resizing',
:retire => 'Retirement',
:revert_to_snapshot => 'Revert Snapshot Operation',
:smartstate_analysis => 'Smartstate Analysis',
:snapshot_create => 'Create Snapshot',
:snapshots => 'Snapshots',
:shutdown_guest => 'Shutdown Guest Operation',
:start => 'Start',
:suspend => 'Suspending',
:terminate => 'Terminate a VM',
:timeline => 'Query for events',
:update_aggregate => 'Host Aggregate Update',
:update => 'Update',
:update_floating_ip => 'Update Floating IP association',
:update_network_router => 'Network Router Update',
:ems_network_new => 'New EMS Network Provider',
:update_security_group => 'Security Group Update',
:block_storage => 'Block Storage',
:object_storage => 'Object Storage',
}.freeze
# Whenever this mixin is included we define all features as unsupported by default.
# This way we can query for every feature
included do
QUERYABLE_FEATURES.keys.each do |feature|
supports_not(feature)
if respond_to?(:virtual_column)
virtual_column("supports_#{feature}", :type => :boolean)
define_method("supports_#{feature}") do
public_send("supports_#{feature}?")
end
end
end
end
class UnknownFeatureError < StandardError; end
def self.guard_queryable_feature(feature)
unless QUERYABLE_FEATURES.key?(feature.to_sym)
raise UnknownFeatureError, "Feature ':#{feature}' is unknown to SupportsFeatureMixin."
end
end
def self.reason_or_default(reason)
reason.present? ? reason : _("Feature not available/supported")
end
# query instance for the reason why the feature is unsupported
def unsupported_reason(feature)
SupportsFeatureMixin.guard_queryable_feature(feature)
feature = feature.to_sym
public_send("supports_#{feature}?") unless unsupported.key?(feature)
unsupported[feature]
end
# query the instance if the feature is supported or not
def supports?(feature)
SupportsFeatureMixin.guard_queryable_feature(feature)
public_send("supports_#{feature}?")
end
# query the instance if a feature is generally known
def feature_known?(feature)
self.class.feature_known?(feature)
end
private
# used inside a +supports+ block to add a reason why the feature is not supported
# just adding a reason will make the feature unsupported
def unsupported_reason_add(feature, reason = nil)
SupportsFeatureMixin.guard_queryable_feature(feature)
feature = feature.to_sym
unsupported[feature] = SupportsFeatureMixin.reason_or_default(reason)
end
def unsupported
@unsupported ||= {}
end
class_methods do
# This is the DSL used a class level to define what is supported
def supports(feature, &block)
SupportsFeatureMixin.guard_queryable_feature(feature)
define_supports_feature_methods(feature, &block)
end
# supports_not does not take a block, because its never supported
# and not conditionally supported
def supports_not(feature, reason: nil)
SupportsFeatureMixin.guard_queryable_feature(feature)
define_supports_feature_methods(feature, :is_supported => false, :reason => reason)
end
# query the class if the feature is supported or not
def supports?(feature)
SupportsFeatureMixin.guard_queryable_feature(feature)
public_send("supports_#{feature}?")
end
# query the class for the reason why something is unsupported
def unsupported_reason(feature)
SupportsFeatureMixin.guard_queryable_feature(feature)
feature = feature.to_sym
public_send("supports_#{feature}?") unless unsupported.key?(feature)
unsupported[feature]
end
# query the class if a feature is generally known
def feature_known?(feature)
SupportsFeatureMixin::QUERYABLE_FEATURES.key?(feature.to_sym)
end
private
def unsupported
# This is a class variable and it might be modified during runtime
# because we dont eager load all classes at boot time, so it needs to be thread safe
@unsupported ||= Concurrent::Hash.new
end
# use this for making a class not support a feature
def unsupported_reason_add(feature, reason = nil)
SupportsFeatureMixin.guard_queryable_feature(feature)
feature = feature.to_sym
unsupported[feature] = SupportsFeatureMixin.reason_or_default(reason)
end
def define_supports_feature_methods(feature, is_supported: true, reason: nil, &block)
method_name = "supports_#{feature}?"
feature = feature.to_sym
# defines the method on the instance
define_method(method_name) do
unsupported.delete(feature)
if block_given?
begin
instance_eval(&block)
rescue => e
_log.log_backtrace(e)
unsupported_reason_add(feature, "Internal Error: #{e.message}")
end
else
unsupported_reason_add(feature, reason) unless is_supported
end
!unsupported.key?(feature)
end
# defines the method on the class
define_singleton_method(method_name) do
unsupported.delete(feature)
# TODO: durandom - make reason evaluate in class context, to e.g. include the name of a subclass (.to_proc?)
unsupported_reason_add(feature, reason) unless is_supported
!unsupported.key?(feature)
end
end
end
end