-
Notifications
You must be signed in to change notification settings - Fork 897
/
Copy pathsupports_feature_mixin.rb
137 lines (122 loc) · 4.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
#
# 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
# 'It is 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 a feature is not supported, return a string for the reason. A nil means it is supported
# The reason will be accessible through
#
# instance.unsupported_reason(:feature)
#
# Post.supports?(:publish) # => true
# Post.new.supports?(:publish) # => true
# 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) # => "It is too good"
#
# 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
#
module SupportsFeatureMixin
extend ActiveSupport::Concern
# Whenever this mixin is included we define all features as unsupported by default.
# This way we can query for every feature
included do
class_attribute :supports_features, :instance_writer => false, :instance_reader => false, :default => {}
end
def self.default_supports_reason
_("Feature not available/supported")
end
# query instance for the reason why the feature is unsupported
def unsupported_reason(feature)
self.class.unsupported_reason(feature, :instance => self)
end
# query the instance if the feature is supported or not
def supports?(feature)
!unsupported_reason(feature)
end
private
class_methods do
# This is the DSL used a class level to define what is supported
def supports(feature, &block)
self.supports_features = supports_features.merge(feature.to_sym => block || true)
end
# supports_not does not take a block, because its never supported
# and not conditionally supported
def supports_not(feature, reason: nil)
self.supports_features = supports_features.merge(feature.to_sym => reason.presence || false)
end
def supports?(feature)
!unsupported_reason(feature)
end
# query the class if the feature is supported or not
def unsupported_reason(feature, instance: self)
# undeclared features are not supported
value = supports_features[feature.to_sym]
if value.respond_to?(:call)
begin
# for class level supports, blocks are not evaluated and assumed to be true
result = instance.instance_eval(&value) unless instance.kind_of?(Class)
result if result.kind_of?(String)
rescue => e
_log.log_backtrace(e)
"Internal Error: #{e.message}"
end
elsif value != true
value || SupportsFeatureMixin.default_supports_reason
end
end
# all subclasses that are considered for supporting features
def supported_subclasses
descendants
end
def subclasses_supporting(feature)
supported_subclasses.select { |subclass| subclass.supports?(feature) }
end
def types_supporting(feature)
subclasses_supporting(feature).map(&:name)
end
# Provider classes that support this feature
def provider_classes_supporting(feature)
subclasses_supporting(feature).map(&:module_parent)
end
# scope to query all those classes that support a particular feature
def supporting(feature)
# First find all instances where the class supports <feature> then select instances
# which also support <feature> (e.g. the supports block does not add an unsupported_reason)
where(:type => types_supporting(feature)).select { |instance| instance.supports?(feature) }
end
# Providers that support this feature
#
# example:
# Host.providers_supporting(feature) # => [Ems]
def providers_supporting(feature)
ExtManagementSystem.where(:type => provider_classes_supporting(feature).map(&:name))
end
end
end