Skip to content
This repository has been archived by the owner on Apr 22, 2022. It is now read-only.

Latest commit

 

History

History
162 lines (117 loc) · 8.4 KB

WRITING_PLUGINS.md

File metadata and controls

162 lines (117 loc) · 8.4 KB

Writing Plugins

Farscape Plugins can be used to add drop-in functionality to Farscape.

Farscape Plugin Hash

A Farscape plugin is described with a hash with a set of well defined keys

name

This is a symbol representing the name of the plugin

type

The type of plugin, this is useful for manipulating multiple plguins simultaneously

middleware

Middleware objects as per Faraday plugins

extensions

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

default_state

When registered, is this :enabled or :disabled by default it is :enabled

Example

{
  name: :PluginName
  type: :example_type
  middleware: PluginMiddlewareClass
  extension: { :Agent => [ExtenstionClass], :RepresentorAgent => [ExtensionClass, Walrus] }
  default_state: :enabled
}

Registration

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.

Managing Plugins

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.

Introspecting plguins

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.

Enabling and Disabling 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.

Example Enable / Disable Workflow

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 on instances

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.

Enabling and Disabling Plugins for Instances

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.

Example instance workflow

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]}]

Adding Middleware

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].

Extending Agent

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!

Farscape Utilities

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.

Creating a Client

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.