farscape 1.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|