Farscape Plugins can be used to add drop-in functionality to Farscape.
A Farscape plugin is described with a hash with a set of well defined keys
This is a symbol representing the name of the plugin
The type of plugin, this is useful for manipulating multiple plguins simultaneously
Middleware objects as per Faraday plugins
A hash whos keys are the Farscape clases to be extended and values are a list of classes which will extend the default functionality of Farscape clases.
Possible keys are: :Agent
, :HttpClient
, :SafeRepresentorAgent
, :RepresentorAgent
, and :TransitionAgent
When registered, is this :enabled or :disabled by default it is :enabled
{
name: :PluginName
type: :example_type
middleware: PluginMiddlewareClass
extension: { :Agent => [ExtenstionClass], :RepresentorAgent => [ExtensionClass, Walrus] }
default_state: :enabled
}
To register your plugin, run
Farscape.register_plugin(name: :Cachinator, type: :cache, ...)
Feel free to put this code at the bottom of cachinator.rb
so that it turns on automatically after the gem is loaded. Consumers who want control can include a line in their initializer reading Farscape.disable!(:Cachinator)
or Farscape.disable!(:cache)
. If you'd rather have your plugin be off by default, you could instead wrap the register_plugin call in, say, a Cachinator.activate
method for the consumer to call as desired. If your plugin is http-specific (say it adds Authentication headers), include protocol: :http
.
Of the set of registered plugins, some will be enabled, and others disabled. Enabled plugins will apply their specified functionality to any current Farscape instance.
You can get a list of plugins with the Farscape.plugins
method. If you want only the enabled plugins you can use Farscape.enabled_plugins
, and if you want only disabled plguins you can do Farscape.disabled_plugins
.
The default state of a plugin is defined by the Plugin Hash. To enable a disabled plugin you need to call Farscape.enable!(options)
where options is a hash containing a :name
key or a :type
key. The name key will only enable plugins with the matching name, whereas type will enable all plugins of that type.
Likewise you can disable plugins with Farscape.disable!(options)
, in which case you disable the plugins.
Farscape.register_plugin({name: :Peacekeeper, type: :sebacean, middleware: [TestMiddleware::NoGetNoProblem]})
Farscape.plugins #=> [{name: :Peacekeeper, type: :sebacean, middleware: [TestMiddleware::NoGetNoProblem]}]
Farscape.enabled_plugins #=> [{name: :Peacekeeper, type: :sebacean, middleware: [TestMiddleware::NoGetNoProblem]}]
Farscape.disabled_plugins #=> []
Farscape.disable!(type: :sebacean)
Farscape.enabled_plugins #=> []
Farscape.disabled_plugins #=> [{name: :Peacekeeper, type: :sebacean, middleware: [TestMiddleware::NoGetNoProblem]}]
Farscape.enable!(name: :Peacekeeper)
Farscape.enabled_plugins #=> [{name: :Peacekeeper, type: :sebacean, middleware: [TestMiddleware::NoGetNoProblem]}]
Farscape.disabled_plugins #=> []
Plugins can also be modified on a per instance basis. Agent, SafeRepresentorAgent, and Representor all support #plugins, #enabled_plugins, and #disabled_plugins on a per instance basis. These behave the same as their associated Farscape class methods, except they communicate which plugins are enabled or diables for that instance only.
When an object creates a new object (for example Agent.new(entry).enter
returns a RepresentorAgent), those instances will have the same plugins enabled as their parent.
To enable plugins on an instance invoke the using(name_or_type)
method, to disable plugins on an instance invoke the omitting(name_or_type)
method. These methods will return a new instance with the associated plugin(s) in their new state. They do not modify the original object.
Farscape.enabled_plugins #=> [{name: :Peacekeeper, type: :sebacean, middleware: [TestMiddleware::NoGetNoProblem]}]
agent = Farscape.Agent.new.omitting(name: :Peacekeeper)
agent.plugins # => [{name: :Peacekeeper, type: :sebacean, middleware: [TestMiddleware::NoGetNoProblem]}]
agent.enabled_plugins # => []
agent.plugins # => [{name: :Peacekeeper, type: :sebacean, middleware: [TestMiddleware::NoGetNoProblem]}]
resource = agent.enter(entry_point).transitions[:listing].invoke
resource.plugins # => [{name: :Peacekeeper, type: :sebacean, middleware: [TestMiddleware::NoGetNoProblem]}]
resource.enabled_plugins # => []
resource.plugins # => [{name: :Peacekeeper, type: :sebacean, middleware: [TestMiddleware::NoGetNoProblem]}]
details = resource.using(name: :Peacekeeper).transitions[:items][0].invoke
details.plugins # => [{name: :Peacekeeper, type: :sebacean, middleware: [TestMiddleware::NoGetNoProblem]}]
details.enabled_plugins # => []
details.disabled_plugins # => [{name: :Peacekeeper, type: :sebacean, middleware: [TestMiddleware::NoGetNoProblem]}]
You can probably do what you need to do by writing Faraday-style middleware. Middleware can inspect and alter outgoing requests and incoming responses, abort requests, and define hooks that run after a request/response cycle completes. All it needs to do is obey a simple API.
To add your middleware to the stack, run
Farscape.register_plugin(name: :Cachinator, type: :cache, middleware: [Cachinator::Middleware], ...)
If you need to partially order your middleware, the elements of the middleware array can be hashes of the form:
{ class: Cachinator::Middleware,
before: RequestSigner,
after: ['HubSubscriber', 'Ouroborous', :authorization]
}
In this example, Cachinator::Middleware will be inserted before the RequestSigner middleware if it is present, and after the latest of HubSubscriber, Ouroborous, or any middleware of type "authorization". Note that Middleware classes can and should be given as strings if your plugin is not providing them, so that Ruby won't throw a NameError if they are undefined. If the ordering constraints given are impossible to satisfy, Farscape will throw an error.
You can also add config to your middleware when passing it in hash form:
{ class: Cachinator::Middleware,
config: MyApp.config[:cache]
}
The config hash will be passed to your middleware as a second argument to new
, as in Faraday. To pass multiple arguments, use config: [arg1, arg2]
.
If you want to provide a more interactive API, or reference the deserialized response using the Representor interface, you can define a module that will be available to mix in to Farscape::Agent.
module Peacekeeper
def pacify!
raise if representor.transitions.keys.include?(:attack)
end
end
Farscape.register_plugin(name: :Peacekeeper, type: :security, extensions: [Peacekeeper], extends: [:Agent])
agent.enter(url).using(:Peacekeeper).pacify!
Any plugin can reference Farscape.cache
, which exposes the same API as Rails.cache, Farscape.logger
, which exposes the same API as the built-in Ruby logger. By default, Farscape.cache operates in-memory and Farscape.logger writes to STDOUT. Plugins can provide enhanced versions of these utilities by modifying the global state of the Farscape object:
module Peacekeeper
class DalliCache
# code that implements the Cache api
end
end
Farscape.cache = Peacekeeper::DalliCache.new(config)
Future updates will provide Farscape.jobs, a backgrounding utility along similar lines.
By default, Farscape uses the Net::HTTP library to make HTTP requests. You can replace this client with Faraday.clients[:http] = MyClient
or define one for a new protocol with Faraday.clients[:amqp] = Jessica::Rabbit
. When a Farscape agent follows a link with a given protocol, it will use the client for that protocol if one has been provided. Required interface tk.