farscape 1.2.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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +14 -0
- data/CONTRIBUTING.md +3 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +192 -0
- data/LICENSE.md +19 -0
- data/README.md +189 -0
- data/Rakefile +10 -0
- data/WRITING_PLUGINS.md +162 -0
- data/lib/farscape/agent.rb +113 -0
- data/lib/farscape/base_agent.rb +15 -0
- data/lib/farscape/cache.rb +13 -0
- data/lib/farscape/client/base_client.rb +27 -0
- data/lib/farscape/client/http_client.rb +99 -0
- data/lib/farscape/clients.rb +9 -0
- data/lib/farscape/errors.rb +172 -0
- data/lib/farscape/helpers/partially_ordered_list.rb +75 -0
- data/lib/farscape/logger.rb +13 -0
- data/lib/farscape/plugins.rb +71 -0
- data/lib/farscape/representor.rb +110 -0
- data/lib/farscape/transition.rb +81 -0
- data/lib/farscape/version.rb +6 -0
- data/lib/farscape.rb +4 -0
- data/lib/plugins/plugins.rb +104 -0
- data/spec/lib/farscape/cache_spec.rb +22 -0
- data/spec/lib/farscape/integration/entry_point_spec.rb +29 -0
- data/spec/lib/farscape/integration/interface_spec.rb +222 -0
- data/spec/lib/farscape/integration/representor_spec.rb +36 -0
- data/spec/lib/farscape/integration/resource_crud_spec.rb +92 -0
- data/spec/lib/farscape/logger_spec.rb +23 -0
- data/spec/lib/farscape/plugins_spec.rb +344 -0
- data/spec/lib/farscape/transition_spec.rb +79 -0
- data/spec/lib/helpers/partially_ordered_list_spec.rb +125 -0
- data/spec/spec_helper.rb +40 -0
- data/spec/support/helpers.rb +5 -0
- data/tasks/yard.rake +15 -0
- metadata +221 -0
@@ -0,0 +1,113 @@
|
|
1
|
+
require 'farscape/representor'
|
2
|
+
require 'farscape/clients'
|
3
|
+
|
4
|
+
module Farscape
|
5
|
+
class Agent
|
6
|
+
|
7
|
+
include BaseAgent
|
8
|
+
|
9
|
+
PROTOCOL = :http
|
10
|
+
|
11
|
+
attr_reader :media_type
|
12
|
+
attr_reader :entry_point
|
13
|
+
|
14
|
+
def initialize(entry = nil, media = :hale, safe = false, plugin_hash = {})
|
15
|
+
@entry_point = entry
|
16
|
+
@media_type = media
|
17
|
+
@safe_mode = safe
|
18
|
+
@plugin_hash = plugin_hash.empty? ? default_plugin_hash : plugin_hash
|
19
|
+
handle_extensions
|
20
|
+
end
|
21
|
+
|
22
|
+
def representor
|
23
|
+
safe? ? SafeRepresentorAgent : RepresentorAgent
|
24
|
+
end
|
25
|
+
|
26
|
+
def enter(entry = entry_point)
|
27
|
+
@entry_point ||= entry
|
28
|
+
raise "No Entry Point Provided!" unless entry
|
29
|
+
response = client.invoke(url: entry, headers: get_accept_header(media_type))
|
30
|
+
find_exception(response)
|
31
|
+
end
|
32
|
+
|
33
|
+
def find_exception(response)
|
34
|
+
error = client.dispatch_error(response)
|
35
|
+
begin
|
36
|
+
representing = representor.new(media_type, response, self)
|
37
|
+
rescue JSON::ParserError
|
38
|
+
representing = response
|
39
|
+
end
|
40
|
+
raise error.new(representing) if error
|
41
|
+
representing
|
42
|
+
end
|
43
|
+
|
44
|
+
# TODO share this information with serialization factory base
|
45
|
+
def get_accept_header(media_type)
|
46
|
+
media_types = { hale: 'application/vnd.hale+json' }
|
47
|
+
{ 'Accept' => media_types[media_type] }
|
48
|
+
end
|
49
|
+
|
50
|
+
def client
|
51
|
+
Farscape.clients[PROTOCOL].new(self)
|
52
|
+
end
|
53
|
+
|
54
|
+
def safe
|
55
|
+
self.class.new(@entry_point, @media_type, true, @plugin_hash)
|
56
|
+
end
|
57
|
+
|
58
|
+
def unsafe
|
59
|
+
self.class.new(@entry_point, @media_type, false, @plugin_hash)
|
60
|
+
end
|
61
|
+
|
62
|
+
def safe?
|
63
|
+
@safe_mode
|
64
|
+
end
|
65
|
+
|
66
|
+
def plugins
|
67
|
+
@plugin_hash[:plugins]
|
68
|
+
end
|
69
|
+
|
70
|
+
def enabled_plugins
|
71
|
+
Plugins.enabled_plugins(@plugin_hash[:plugins])
|
72
|
+
end
|
73
|
+
|
74
|
+
def disabled_plugins
|
75
|
+
Plugins.disabled_plugins(@plugin_hash[:plugins])
|
76
|
+
end
|
77
|
+
|
78
|
+
def middleware_stack
|
79
|
+
@plugin_hash[:middleware_stack] ||= Plugins.construct_stack(enabled_plugins)
|
80
|
+
end
|
81
|
+
|
82
|
+
def using(name_or_type)
|
83
|
+
disabling_rules, plugins = Plugins.enable(name_or_type, @plugin_hash[:disabling_rules], @plugin_hash[:plugins])
|
84
|
+
plugin_hash = {
|
85
|
+
disabling_rules: disabling_rules,
|
86
|
+
plugins: plugins,
|
87
|
+
middleware_stack: nil
|
88
|
+
}
|
89
|
+
self.class.new(@entry_point, @media_type, @safe_mode, plugin_hash)
|
90
|
+
end
|
91
|
+
|
92
|
+
def omitting(name_or_type)
|
93
|
+
disabling_rules, plugins = Plugins.disable(name_or_type, @plugin_hash[:disabling_rules], @plugin_hash[:plugins])
|
94
|
+
plugin_hash = {
|
95
|
+
disabling_rules: disabling_rules,
|
96
|
+
plugins: plugins,
|
97
|
+
middleware_stack: nil
|
98
|
+
}
|
99
|
+
self.class.new(@entry_point, @media_type, @safe_mode, plugin_hash)
|
100
|
+
end
|
101
|
+
|
102
|
+
private
|
103
|
+
|
104
|
+
def default_plugin_hash
|
105
|
+
{
|
106
|
+
plugins: Farscape.plugins.dup, # A hash of plugins keyed by the plugin name
|
107
|
+
disabling_rules: Farscape.disabling_rules.dup, # A list of symbols that are Names or types of plugins
|
108
|
+
middleware_stack: nil
|
109
|
+
}
|
110
|
+
end
|
111
|
+
|
112
|
+
end
|
113
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Farscape
|
2
|
+
module BaseAgent
|
3
|
+
|
4
|
+
def handle_extensions
|
5
|
+
extensions = Plugins.extensions(enabled_plugins)
|
6
|
+
extensions = extensions[self.class.to_s.split(':')[-1].to_sym]
|
7
|
+
extensions.each { |cls| self.extend(cls) } if extensions
|
8
|
+
end
|
9
|
+
|
10
|
+
%w(disabled_plugins enabled_plugins plugins).each do |meth|
|
11
|
+
define_method(meth) { @agent.send(meth) }
|
12
|
+
end
|
13
|
+
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Farscape
|
2
|
+
class Agent
|
3
|
+
# Client independent of protocol, only used for HTTP for now
|
4
|
+
class BaseClient
|
5
|
+
|
6
|
+
def interface_methods
|
7
|
+
{
|
8
|
+
safe: [],
|
9
|
+
unsafe: [],
|
10
|
+
idempotent: []
|
11
|
+
}
|
12
|
+
end
|
13
|
+
|
14
|
+
def safe_method?(meth)
|
15
|
+
interface_methods[:safe].include?(meth)
|
16
|
+
end
|
17
|
+
|
18
|
+
def unsafe_method?(meth)
|
19
|
+
interface_methods[:unsafe].include?(meth)
|
20
|
+
end
|
21
|
+
|
22
|
+
def idempotent_method?(meth)
|
23
|
+
interface_methods[:idempotent].include?(meth)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
require 'faraday'
|
2
|
+
require 'farscape/client/base_client'
|
3
|
+
require 'farscape/plugins'
|
4
|
+
require 'farscape/errors'
|
5
|
+
|
6
|
+
module Farscape
|
7
|
+
class Agent
|
8
|
+
class HTTPClient < BaseClient
|
9
|
+
|
10
|
+
# The Faraday connection instance.
|
11
|
+
attr_reader :connection
|
12
|
+
attr_reader :Plugins
|
13
|
+
|
14
|
+
def initialize(agent)
|
15
|
+
@connection = Faraday.new do |builder|
|
16
|
+
builder.request :url_encoded
|
17
|
+
agent.middleware_stack.each do |middleware|
|
18
|
+
if middleware.key?(:config)
|
19
|
+
config = middleware[:config]
|
20
|
+
if config.is_a?(Array)
|
21
|
+
builder.use(middleware[:class], *config)
|
22
|
+
else
|
23
|
+
builder.use(middleware[:class], config)
|
24
|
+
end
|
25
|
+
else
|
26
|
+
builder.use(middleware[:class])
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
builder.adapter faraday_adapter
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# Override this in a subclass to create clients with custom Faraday adapters
|
35
|
+
def faraday_adapter
|
36
|
+
Faraday.default_adapter
|
37
|
+
end
|
38
|
+
|
39
|
+
##
|
40
|
+
# Makes a Faraday request given the specified options
|
41
|
+
#
|
42
|
+
# @options [Hash] The hash of Faraday options passed to the request, including url, method,
|
43
|
+
# params, body, and headers.
|
44
|
+
# @return [Faraday::Response] The response object resulting from the Faraday call
|
45
|
+
def invoke(options = {})
|
46
|
+
defaults = { method: 'get'}
|
47
|
+
options = defaults.merge(options)
|
48
|
+
|
49
|
+
connection.send(options[:method].to_s.downcase) do |req|
|
50
|
+
req.url options[:url]
|
51
|
+
req.body = options[:body] if options.has_key?(:body)
|
52
|
+
options[:params].each { |k,v| req.params[k] = v } if options.has_key?(:params)
|
53
|
+
options[:headers].each { |k,v| req.headers[k] = v } if options.has_key?(:headers)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def interface_methods
|
58
|
+
{
|
59
|
+
idempotent: ['PUT', 'DELETE'],
|
60
|
+
unsafe: ['POST', 'PATCH'], # http://tools.ietf.org/html/rfc5789
|
61
|
+
safe: ['GET', 'HEAD', 'OPTIONS', 'TRACE', 'CONNECT']
|
62
|
+
}
|
63
|
+
end
|
64
|
+
|
65
|
+
def dispatch_error(response)
|
66
|
+
errors = Farscape::Exceptions
|
67
|
+
http_code = {
|
68
|
+
400 => errors::BadRequest,
|
69
|
+
401 => errors::Unauthorized,
|
70
|
+
403 => errors::Forbidden,
|
71
|
+
404 => errors::NotFound,
|
72
|
+
405 => errors::MethodNotAllowed,
|
73
|
+
406 => errors::NotAcceptable,
|
74
|
+
407 => errors::ProxyAuthenticationRequired,
|
75
|
+
408 => errors::RequestTimeout,
|
76
|
+
409 => errors::Conflict,
|
77
|
+
410 => errors::Gone,
|
78
|
+
411 => errors::LengthRequired,
|
79
|
+
412 => errors::PreconditionFailed,
|
80
|
+
413 => errors::RequestEntityTooLarge,
|
81
|
+
414 => errors::RequestUriTooLong,
|
82
|
+
415 => errors::UnsupportedMediaType,
|
83
|
+
416 => errors::RequestedRangeNotSatisfiable,
|
84
|
+
417 => errors::ExpectationFailed,
|
85
|
+
418 => errors::ImaTeapot,
|
86
|
+
422 => errors::UnprocessableEntity,
|
87
|
+
500 => errors::InternalServerError,
|
88
|
+
501 => errors::NotImplemented,
|
89
|
+
502 => errors::BadGateway,
|
90
|
+
503 => errors::ServiceUnavailable,
|
91
|
+
504 => errors::GatewayTimeout,
|
92
|
+
505 => errors::ProtocolVersionNotSupported,
|
93
|
+
}
|
94
|
+
http_code[response.status] || errors::ProtocolException unless response.success?
|
95
|
+
end
|
96
|
+
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,172 @@
|
|
1
|
+
require 'active_support/core_ext/string/filters'
|
2
|
+
|
3
|
+
module Farscape
|
4
|
+
module Exceptions
|
5
|
+
class ProtocolException < IOError
|
6
|
+
attr_reader :representor
|
7
|
+
|
8
|
+
def initialize(representor)
|
9
|
+
@representor = representor
|
10
|
+
end
|
11
|
+
|
12
|
+
def message
|
13
|
+
@representor.representor.to_hash
|
14
|
+
end
|
15
|
+
|
16
|
+
def error_description
|
17
|
+
'Unknown Error'
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
#4xx
|
22
|
+
class BadRequest < ProtocolException
|
23
|
+
def error_description
|
24
|
+
'The request could not be understood by the server due to malformed syntax. The client SHOULD NOT repeat the request without modifications.'
|
25
|
+
end
|
26
|
+
end
|
27
|
+
class Unauthorized < ProtocolException
|
28
|
+
def error_description
|
29
|
+
'The request requires user authentication.
|
30
|
+
The client MAY repeat the request with suitable Authorization.
|
31
|
+
If the request already included Authorization credentials,
|
32
|
+
then the response indicates that authorization has been refused for those credentials.'.squish
|
33
|
+
end
|
34
|
+
end
|
35
|
+
class Forbidden < ProtocolException
|
36
|
+
def error_description
|
37
|
+
'The server understood the request, but is refusing to fulfill it.
|
38
|
+
Authorization will not help and the request SHOULD NOT be repeated.'.squish
|
39
|
+
end
|
40
|
+
end
|
41
|
+
class NotFound < ProtocolException
|
42
|
+
def error_description
|
43
|
+
'The server has not found anything matching the Request-URI.
|
44
|
+
No indication is given of whether the condition is temporary or permanent.
|
45
|
+
This status code is commonly used when the server does not wish to reveal exactly why the request has been refused, or when no other response is applicable.'.squish
|
46
|
+
end
|
47
|
+
end
|
48
|
+
class MethodNotAllowed < ProtocolException
|
49
|
+
def error_description
|
50
|
+
'The protocol method specified in the Request-Line is not allowed for the resource identified by the Request-URI.'
|
51
|
+
end
|
52
|
+
end
|
53
|
+
class NotAcceptable < ProtocolException
|
54
|
+
def error_description
|
55
|
+
'The resource identified by the request is only capable of generating response entities which have content characteristics not acceptable according to the accept headers sent in the request.'
|
56
|
+
end
|
57
|
+
end
|
58
|
+
class ProxyAuthenticationRequired < ProtocolException
|
59
|
+
def error_description
|
60
|
+
'The client must first authenticate itself with the proxy.
|
61
|
+
The client MAY repeat the request with suitable Proxy Authorization.'.squish
|
62
|
+
end
|
63
|
+
end
|
64
|
+
class RequestTimeout < ProtocolException
|
65
|
+
def error_description
|
66
|
+
'The client did not produce a request within the time that the server was prepared to wait.
|
67
|
+
The client MAY repeat the request without modifications at any later time.'.squish
|
68
|
+
end
|
69
|
+
end
|
70
|
+
class Conflict < ProtocolException
|
71
|
+
def error_description
|
72
|
+
'The request could not be completed due to a conflict with the current state of the resource.
|
73
|
+
This code is only allowed in situations where it is expected that the user might be able to resolve the conflict and resubmit the request.
|
74
|
+
Conflicts are most likely to occur in response to an idempotent request.
|
75
|
+
For example, if versioning were being used and the entity included changes to a resource which conflict with those made by an earlier (third-party) request'.squish
|
76
|
+
end
|
77
|
+
end
|
78
|
+
class Gone < ProtocolException
|
79
|
+
def error_description
|
80
|
+
'The requested resource is no longer available at the server and no forwarding address is known.
|
81
|
+
This condition is expected to be considered permanent.
|
82
|
+
Clients with link editing capabilities SHOULD delete references to the Request-URI after user approval.
|
83
|
+
This response is cacheable unless indicated otherwise.'.squish
|
84
|
+
end
|
85
|
+
end
|
86
|
+
class LengthRequired < ProtocolException
|
87
|
+
def error_description
|
88
|
+
'The server refuses to accept the request without a defined content length.'
|
89
|
+
end
|
90
|
+
end
|
91
|
+
class PreconditionFailed < ProtocolException
|
92
|
+
def error_description
|
93
|
+
'The precondition given by the client evaluated to false when it was tested on the server.'
|
94
|
+
end
|
95
|
+
end
|
96
|
+
class RequestEntityTooLarge < ProtocolException
|
97
|
+
def error_description
|
98
|
+
'The server is refusing to process a request because the request entity is larger than the server is willing or able to process.'.squish
|
99
|
+
end
|
100
|
+
end
|
101
|
+
class RequestUriTooLong < ProtocolException
|
102
|
+
def error_description
|
103
|
+
'The server is refusing to service the request because the Request-URI is longer than the server is willing to interpret.'
|
104
|
+
end
|
105
|
+
end
|
106
|
+
class UnsupportedMediaType < ProtocolException
|
107
|
+
def error_description
|
108
|
+
'The server is refusing to service the request because the entity of the request is in a format not supported by the requested resource for the requested method.'
|
109
|
+
end
|
110
|
+
end
|
111
|
+
class RequestedRangeNotSatisfiable < ProtocolException
|
112
|
+
def error_description
|
113
|
+
'A request requested a resource within a range,
|
114
|
+
and none of the range-specifier values in this field overlap the current extent of the selected resource,
|
115
|
+
and the request did not specify range conditions.'.squish
|
116
|
+
end
|
117
|
+
end
|
118
|
+
class ExpectationFailed < ProtocolException
|
119
|
+
def error_description
|
120
|
+
'The expectation given by the client could not be met by this server.
|
121
|
+
If the server is a proxy, the server has unambiguous evidence that the request could not be met by the next-hop server.'.squish
|
122
|
+
end
|
123
|
+
end
|
124
|
+
class ImaTeapot < ProtocolException
|
125
|
+
def error_description
|
126
|
+
'The server is a teapot; the resulting entity body may be short and stout.
|
127
|
+
Demonstrations of this behaviour exist.'.squish
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
class UnprocessableEntity < ProtocolException
|
132
|
+
def error_description
|
133
|
+
'The request was well-formed but was unable to be followed due to semantic errors.'.squish
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
#5xx
|
138
|
+
class InternalServerError < ProtocolException
|
139
|
+
def error_description
|
140
|
+
'The server encountered an unexpected condition which prevented it from fulfilling the request.'
|
141
|
+
end
|
142
|
+
end
|
143
|
+
class NotImplemented < ProtocolException
|
144
|
+
def error_description
|
145
|
+
'The server does not support the functionality required to fulfill the request.'
|
146
|
+
end
|
147
|
+
end
|
148
|
+
class BadGateway < ProtocolException
|
149
|
+
def error_description
|
150
|
+
'The server received an invalid response from the upstream server it accessed in attempting to fulfill the request.'
|
151
|
+
end
|
152
|
+
end
|
153
|
+
class ServiceUnavailable < ProtocolException
|
154
|
+
def error_description
|
155
|
+
'The server is currently unable to handle the request due to a temporary overloading or maintenance of the server.
|
156
|
+
The implication is that this is a temporary condition which will be alleviated after some delay.'.squish
|
157
|
+
end
|
158
|
+
end
|
159
|
+
class GatewayTimeout < ProtocolException
|
160
|
+
def error_description
|
161
|
+
'The server did not receive a timely response from an upstream server specified it needed to access in attempting to complete the request.'
|
162
|
+
end
|
163
|
+
end
|
164
|
+
class ProtocolVersionNotSupported < ProtocolException
|
165
|
+
def error_description
|
166
|
+
'The server does not support, or refuses to support, the protocol or protocol version that was used in the request message.
|
167
|
+
The server is indicating that it is unable or unwilling to complete the request using the same protocol as the client'.squish
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
end
|
172
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
# Convenience class that finds an order for a set of elements that satisfies an arbitrary sorting function that
|
2
|
+
# can be undefined for some pairs.
|
3
|
+
# TODO: Optimize and gemify (or find existing gem that does this)
|
4
|
+
|
5
|
+
class PartiallyOrderedList
|
6
|
+
include Enumerable
|
7
|
+
|
8
|
+
class Element < Struct.new(:item, :preceders)
|
9
|
+
def inspect
|
10
|
+
if preceders.any?
|
11
|
+
"#{item} > {#{preceders.map(&:item).join(', ')}}"
|
12
|
+
else
|
13
|
+
item
|
14
|
+
end
|
15
|
+
end
|
16
|
+
def ==(other)
|
17
|
+
item == other.item
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
CircularOrderingError = Class.new(StandardError)
|
22
|
+
|
23
|
+
attr_accessor :elements, :ordering
|
24
|
+
|
25
|
+
def initialize(&block)
|
26
|
+
raise ArgumentError, "#{self.class}.new requires a block" unless block_given?
|
27
|
+
@elements = []
|
28
|
+
@ordering = block
|
29
|
+
end
|
30
|
+
|
31
|
+
def add(item)
|
32
|
+
@cached_ary = nil
|
33
|
+
new_el = Element.new(item, [])
|
34
|
+
elements.each do |old_el|
|
35
|
+
case ordering.call(old_el.item, new_el.item)
|
36
|
+
when -1
|
37
|
+
new_el.preceders << old_el
|
38
|
+
when 1
|
39
|
+
old_el.preceders << new_el
|
40
|
+
end
|
41
|
+
end
|
42
|
+
elements << new_el
|
43
|
+
end
|
44
|
+
|
45
|
+
def delete(item)
|
46
|
+
if element = elements.find { |elt| elt.item == item }
|
47
|
+
elements.delete(element)
|
48
|
+
@cached_ary.delete(element) if @cached_ary
|
49
|
+
elements.each { |elt| elt.preceders.delete(element) }
|
50
|
+
item
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def each(&block)
|
55
|
+
return to_enum unless block_given?
|
56
|
+
if @cached_ary && @cached_ary.size == elements.size
|
57
|
+
@cached_ary.each{ |elt| yield elt.item }
|
58
|
+
else
|
59
|
+
@cached_ary = []
|
60
|
+
unadded = elements.map{ |elt| elt=elt.dup; elt.preceders = elt.preceders.dup; elt }
|
61
|
+
while unadded.any?
|
62
|
+
i = unadded.find_index { |candidate| candidate.preceders.none? }
|
63
|
+
if i
|
64
|
+
to_add = unadded.delete_at(i)
|
65
|
+
yield(to_add.item)
|
66
|
+
unadded.each { |elt| elt.preceders.delete(to_add) }
|
67
|
+
@cached_ary << to_add
|
68
|
+
else
|
69
|
+
raise CircularOrderingError.new("Could not resolve ordering for #{unadded.map(&:item)}")
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
require_relative 'helpers/partially_ordered_list'
|
2
|
+
|
3
|
+
module Farscape
|
4
|
+
|
5
|
+
extend Plugins
|
6
|
+
|
7
|
+
attr_reader :plugins
|
8
|
+
attr_reader :disabling_rules
|
9
|
+
|
10
|
+
@plugins = {} # A hash of plugins keyed by the plugin name
|
11
|
+
@disabling_rules = [] # A list of symbols that are Names or types of plugins
|
12
|
+
@middleware_stack = nil
|
13
|
+
|
14
|
+
def self.plugins
|
15
|
+
@plugins
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.disabling_rules
|
19
|
+
@disabling_rules
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.register_plugin(options)
|
23
|
+
@middleware_stack = nil
|
24
|
+
options[:enabled] = self.enabled?(options)
|
25
|
+
@plugins[options[:name]] = options
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.register_plugins(a_list)
|
29
|
+
a_list.each { |options| register_plugin(options) }
|
30
|
+
end
|
31
|
+
|
32
|
+
# Returns the Poset representing middleware dependency
|
33
|
+
def self.middleware_stack
|
34
|
+
@middleware_stack ||= Plugins.construct_stack(enabled_plugins)
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.enabled_plugins
|
38
|
+
Plugins.enabled_plugins(@plugins)
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.disabled_plugins
|
42
|
+
Plugins.disabled_plugins(@plugins)
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.disabled?(options)
|
46
|
+
Plugins.disabled?(@plugins, @disabling_rules, options)
|
47
|
+
end
|
48
|
+
|
49
|
+
def self.enabled?(options)
|
50
|
+
Plugins.enabled?(@plugins, @disabling_rules, options)
|
51
|
+
end
|
52
|
+
|
53
|
+
# Prevents a plugin from being registered, and disables it if it's already there
|
54
|
+
def self.disable!(name_or_type)
|
55
|
+
@middleware_stack = nil
|
56
|
+
@disabling_rules, @plugins = Plugins.disable(name_or_type, @disabling_rules, @plugins)
|
57
|
+
end
|
58
|
+
|
59
|
+
# Allows a plugin to be registered, and enables it if it's already there
|
60
|
+
def self.enable!(name_or_type)
|
61
|
+
@middleware_stack = nil
|
62
|
+
@disabling_rules, @plugins = Plugins.enable(name_or_type, @disabling_rules, @plugins)
|
63
|
+
end
|
64
|
+
|
65
|
+
# Removes all plugins and disablings of plugins
|
66
|
+
def self.clear
|
67
|
+
@plugins = {}
|
68
|
+
@disabling_rules = []
|
69
|
+
@middleware_stack = nil
|
70
|
+
end
|
71
|
+
end
|