Service implementation authors subclass this to describe the interface that they implement for a particular Resource, as documented in the Loyalty Platform API.
See class method ::interface for details.
- A
- E
- I
- P
- S
- T
- U
- V
[RW] | actions | Supported action methods as a Set of symbols with one or more of
|
[RW] | additional_permissions | A Hash, keyed by String equivalents of the Symbols in Hoodoo::Services::Middleware::ALLOWED_ACTIONS, where the values are Hoodoo::Services::Permissions instances describing extended permissions for the related action. See ::additional_permissions_for. |
[RW] | embeds | Array of strings listing allowed embeddable things. Each string matches the
split up comma-separated value for query string
…would be valid provided there was an embedding declaration such as:
…which would in turn lead this accessor to return:
|
[RW] | endpoint | Endpoint path as declared by service, without preceding “/”, possibly as a
symbol - e.g. |
[RW] | errors_for | A Hoodoo::ErrorDescriptions instance describing all errors that the interface might return, including the default set of platform and generic errors. If nil, there are no additional error codes beyond the default set. |
[RW] | implementation | Implementation class for the service. An Hoodoo::Services::Implementation subclass - the class, not an instance of it. |
[RW] | public_actions | Public action methods as a Set of symbols with one or more of
|
[RW] | resource | Name of the resource the interface addresses as a symbol, e.g.
|
[RW] | secure_log_for | Secure log actions set by secure_log_for - see that call for details. The default is an empty Hash. |
[RW] | to_create | A Hoodoo::Presenters::Object
instance describing the schema for client JSON coming in for calls that
create instances of the resource that the service's interface is
addressing. If |
[RW] | to_update | A Hoodoo::Presenters::Object
instance describing the schema for client JSON coming in for calls that
modify instances of the resource that the service's interface is
addressing. If |
[RW] | version | Major version of interface as an integer. All service endpoint routes have “v{version}/” as a prefix, e.g. “/v1/products”. |
A Hoodoo::Services::Interface::ToList instance describing the list parameters for the interface as a Set of Strings. See also Hoodoo::Services::Interface::ToListDSL.
Source: show
# File lib/hoodoo/services/services/interface.rb, line 879 def to_list @to_list ||= Hoodoo::Services::Interface::ToList.new @to_list end
Define the subclass Service's interface. A DSL is used with methods documented in the Hoodoo::Services::InterfaceDSL class.
The absolute bare minimum interface description just states that a particular implementation class is used when requests are made to a particular URL endpoint, which is implementing an interface for a particular given resource. For a hypothetical Magic resource interface:
class MagicImplementation < Hoodoo::Services::Implementation
# ...implementation code goes here...
end
class MagicInterface < Hoodoo::Services::Interface
interface :Magic do
endpoint :paul_daniels, MagicImplementation
end
end
This would cause all calls to URLs at '/paul_daniels' to be routed to an instance of the MagicImplementation class.
Addtional DSL facilities allow the interface to say what HTTP methods it supports (in terms of the action methods that it supports inside its implementation class), describe any extra sort, search or filter data it allows beyond the common fields and describe the expected JSON fields for creation and/or modification actions. By specifing these, the service middleware code is able to do extra validation and sanitisation of client requests, but they're entirely optional if the implementation class wants to take over all of that itself.
resource
-
Name of the resource that the interface is for, as a String or Symbol (e.g.
:Purchase
). - &block
-
Block that calls the Hoodoo::Services::InterfaceDSL methods; endpoint is the only mandatory call.
Source: show
# File lib/hoodoo/services/services/interface.rb, line 779 def self.interface( resource, &block ) if @to_list.nil? @to_list = Hoodoo::Services::Interface::ToList.new else raise "Hoodoo::Services::Interface subclass unexpectedly ran ::interface more than once" end self.resource = resource.to_sym interface = self.new interface.instance_eval do version 1 embeds # Nothing actions *Hoodoo::Services::Middleware::ALLOWED_ACTIONS public_actions # None secure_log_for # None end interface.instance_eval( &block ) if self.endpoint.nil? raise "Hoodoo::Services::Interface subclasses must always call the 'endpoint' DSL method in their interface descriptions" end end
List the actions that the service implementation supports. If you don't call this, the middleware assumes that all actions are available; else it only calls for supported actions. If you declared an empty array, your implementation would never be called.
- *supported_actions
-
One or more from
:list
,:show
,:create
,:update
and:delete
. Always use symbols, not strings. An exception is raised if unrecognised actions are given.
Example:
actions :list, :show
Source: show
# File lib/hoodoo/services/services/interface.rb, line 373 def actions( *supported_actions ) supported_actions.map! { | item | item.to_sym } invalid = supported_actions - Hoodoo::Services::Middleware::ALLOWED_ACTIONS unless invalid.empty? raise "Hoodoo::Services::Interface#actions does not recognise one or more actions: '#{ invalid.join( ', ' ) }'" end self.class.send( :actions=, Set.new( supported_actions ) ) end
Declare additional permissions that you require for a given action.
If the implementation of a resource endpoint involves making calls out to other resources, then you need to consider how authorisation is granted to those other resources.
The Hoodoo::Services::Session instance for the inbound external caller carries a Hoodoo::Services::Permission instance describing the actions that the caller is permitted to do. The middleware enforces these permissions, so that a resource implementation won't be called at all unless the caller has permission to do so.
These permissions continue to apply during inter-resource calls. The wider session context is always applied. So, if one resource calls another resource, either:
-
The inbound API caller's session must have all necessary permissions for both the resource it is actually directly calling, and for any actions in any resources that the called resource in turn calls (and so-on, for any chain of resources).
…or…
-
The resource uses this
additional_permissions_for
method to declare up-front that it will require the described permissions when a particular action is performed on it. When an inter-resource call is made, a temporary internal-only session is constructed that merges the permissions of the inbound caller with the additional permissions requested by the resource. The downstream called resource needs no special case code at all - it just sees a valid session with valid permissions and does what the upstream resource asked of it.
For example, suppose a resource Clock returns both a time and a date, by
calling out to the Time and Date resources. One option is that the inbound
caller must have show
action permissions for all of Clock,
Time and Date; if any of those are missing, then an attempt to call
show
on the Clock resource would result in a 403 response.
The other option is for Clock's interface to declare its requirements:
additional_permissions_for( :show ) do | p |
p.set_resource( :Time, :show, Hoodoo::Services::Permissions::ALLOW )
p.set_resource( :Date, :show, Hoodoo::Services::Permissions::ALLOW )
end
Suppose you could create Clock instances for some reason, but there was an audit trail for this; Clock must create an Audit entry itself, but you don't want to expose this ability to external callers through their session permissions; so, just declare your additional permissions for that specific inter-service case:
additional_permissions_for( :create ) do | p |
p.set_resource( :Audit, :create, Hoodoo::Services::Permissions::ALLOW )
end
The call says which action in the declaring _interface's_ resource is a target. The block takes a single parameter; this is a default initialisation Hoodoo::Services::Permissions instance. Use that object's methods to set up whatever permissions you need in other resources, to successfully process the action in question. You only need to describe the resources you immediately call, not the whole chain - if “this” resource calls another, then it's up to the other resource to in turn describe additional permissions should it make its own set of downstream calls to further resource endpoints.
Setting default permissions or especially the default permission fallback inside the block is possible but VERY STRONGLY DISCOURAGED. Instead, precisely describe the downstream resources, actions and permissions that are required.
Note an important restriction - public actions (see ::public_actions) cannot be augmented in this way. A public action in one resource can only ever call public actions in other resources. This is because no session is needed at all to call a public action; calling into a protected action in another resource from this context would require invention of a full caller context which would be entirely invented and could represent an accidental (and significant) security hole.
If you call this method for the same action more than once, the last call will be the one that takes effect - each call overwrites the results of any previous call made for the same action.
Parameters are:
action
-
The action in this interface which will require the additional permissions to be described. Pass a Symbol or equivalent String from the list in Hoodoo::Services::Middleware::ALLOWED_ACTIONS.
- &block
-
Block which is passed a new, default state Hoodoo::Services::Permissions instance; make method calls on this instance to describe the required permissions.
Source: show
# File lib/hoodoo/services/services/interface.rb, line 726 def additional_permissions_for( action, &block ) action = action.to_s unless block_given? raise 'Hoodoo::Services::Interface#additional_permissions_for must be passed a block' end p = Hoodoo::Services::Permissions.new yield( p ) additional_permissions = self.class.additional_permissions() || {} additional_permissions[ action ] = p self.class.send( :additional_permissions=, additional_permissions ) end
An array of supported embed keys (as per documentation, so singular or
plural as per resource interface descriptions in the Loyalty Platform API).
Things which can be embedded can also be referenced, via the
_embed
and _reference
query string keys.
The middleware uses the list to reject requests from clients which ask for embedded or referenced entities that were not listed by the interface. If you don't call here, or call here with an empty array, no embedding or referencing will be allowed for calls to the service implementation.
embed
-
Array of permitted embeddable entity names, as symbols or strings. The order of array entries is arbitrary.
Example: An interface permits lists that request embedding or referencing of “vouchers”, “balances” and “member”:
embeds :vouchers, :balances, :member
As a result, embeds would return:
[ 'vouchers', 'balances', 'member' ]
Source: show
# File lib/hoodoo/services/services/interface.rb, line 496 def embeds( *embeds ) self.class.send( :embeds=, embeds.map { | item | item.to_s } ) end
Mandatory part of the interface DSL. Declare the interface's URL endpoint and the Hoodoo::Services::Implementation subclass to be invoked when client requests are sent to a URL matching the endpoint.
No two interfaces can use the same endpoint within a service application, unless the describe a different interface version - see version.
Example:
endpoint :estimations, PurchaseImplementation
uri_path_fragment
-
Path fragment to match at the start of a URL path, as a symbol or string, excluding leading “/”. The URL path matches the fragment if the path starts with a “/”, then matches the fragment exactly, then is followed by either “.”, another “/”, or the end of the path string. For example, a fragment of
:products
matches all paths out of/products
,/products.json
or/products/22
, but does not match/products_and_things
. implementation_class
-
The Hoodoo::Services::Implementation subclass (the class itself, not an instance of it) that should be used when a request matching the path fragment is received.
Source: show
# File lib/hoodoo/services/services/interface.rb, line 333 def endpoint( uri_path_fragment, implementation_class ) # http://www.ruby-doc.org/core-2.2.3/Module.html#method-i-3C # unless implementation_class < Hoodoo::Services::Implementation raise "Hoodoo::Services::Interface#endpoint must provide Hoodoo::Services::Implementation subclasses, but '#{ implementation_class }' was given instead" end self.class.send( :endpoint=, uri_path_fragment ) self.class.send( :implementation=, implementation_class ) end
Declares custom errors that are part of this defined interface. This calls directly through to Hoodoo::ErrorDescriptions#errors_for, so see that for details.
A service should usually define only a single domain of error using one call to errors_for, but techncially can make as many calls for as many domains as required. Definitions are merged.
domain
-
Domain, e.g. 'purchase', 'transaction' - see Hoodoo::ErrorDescriptions#errors_for for details.
- &block
-
Code block making Hoodoo::ErrorDescriptions DSL calls.
Example:
errors_for 'transaction' do
error 'duplicate_transaction', status: 409, message: 'Duplicate transaction', :required => [ :client_uid ]
end
Source: show
# File lib/hoodoo/services/services/interface.rb, line 623 def errors_for( domain, &block ) descriptions = self.class.errors_for if descriptions.nil? descriptions = self.class.send( :errors_for=, Hoodoo::ErrorDescriptions.new ) end descriptions.errors_for( domain, &block ) end
List any actions which are public - NOT PROTECTED BY SESSIONS. For public actions, no X-Session-ID or similar header is consulted and no session data will be associated with your Hoodoo::Services::Context instance when action methods are called.
Use with great care!
Note that if the implementation of a public action needs to call other resources, it can only ever call them if those actions in those other resources are also public. The implementation of a public action is prohibited from making calls to protected actions in other resources.
- *public_actions
-
One or more from
:list
,:show
,:create
,:update
and:delete
. Always use symbols, not strings. An exception is raised if unrecognised actions are given.
Source: show
# File lib/hoodoo/services/services/interface.rb, line 402 def public_actions( *public_actions ) public_actions.map! { | item | item.to_sym } invalid = public_actions - Hoodoo::Services::Middleware::ALLOWED_ACTIONS unless invalid.empty? raise "Hoodoo::Services::Interface#public_actions does not recognise one or more actions: '#{ invalid.join( ', ' ) }'" end self.class.send( :public_actions=, Set.new( public_actions ) ) end
Set secure log actions.
secure_log_actions
-
A Hash, described below.
The given Hash keys are names of actions as Symbols: :list
,
:show
, :create
, :update
or
:delete
. Values are :request
,
:response
or :both
. For a given action targeted
at this resource:
-
A key of
:request
means that API call-related Hoodoo automatic logging will exclude body data for the inbound request, but still include body data in the response. Example: A POST to a Login resource includes a password which you don't want logged, but the response data doesn't quote the password back so is “safe”. The secure log actions Hash for the Login resource's interface would include:create => :request
. -
A key of
:response
means that API call-related Hoodoo automatic logging will exclude body data for the outbound response, but still include body data in the request. Example: A POST to a Caller resource creates a Caller with a generated authentication secret that's only exposed in the POST's response. The inbound data used to create that Caller can be safely logged, but the authentication secret is sensitive and shouldn't be recorded. The secure log actions Hash for the Caller resource's interface would include:create => :response
.ERROR RESPONSES ARE STILL LOGGED because that's useful data; so make sure that if you generate any custom errors in your service that secure data is not contained within them.
-
A key of
both
has the same result as both:request
and:response
, so body data is never logged. It's hard to come up with good examples of resources where both the incoming data is sensitive and the outgoing data is sensitive but the option is included for competion, as someone out there will need it.
Example: The request body data sent by a caller into a resource's
:create
action will not be logged:
secure_log_for( { :create => :request } )
Example: Neither the request data sent by a caller, nor the response data
sent back, will be logged for an :update
action:
secure_log_for( { :update => :both } )
The default is an empty Hash; all actions have both inbound request body data and outbound response body data logged by Hoodoo.
Source: show
# File lib/hoodoo/services/services/interface.rb, line 462 def secure_log_for( secure_log_actions = {} ) secure_log_actions = Hoodoo::Utilities.symbolize( secure_log_actions ) invalid = secure_log_actions.keys - Hoodoo::Services::Middleware::ALLOWED_ACTIONS unless invalid.empty? raise "Hoodoo::Services::Interface#secure_log_for does not recognise one or more actions: '#{ invalid.join( ', ' ) }'" end self.class.send( :secure_log_for=, secure_log_actions ) end
Optional description of the JSON parameters (schema) that the
interface's implementation requires for calls creating resource
instances. The block uses the DSL from Hoodoo::Presenters::Object, so you can
specify basic object things like string
, or higher level
things like type
or resource
.
If a call comes into the middleware from a client which contains body data that doesn't validate according to your schema, it'll be rejected before even getting as far as your interface implementation.
Default values for fields where present are for rendering only; they are not injected into the inbound body for (say) persistence at database levels. A returned, rendered representation based on the same schema would have the default values present only. If you need default values at the persistence layer too, define them there too with whatever mechanism is most appropriate for your chosen persistence approach.
The Hoodoo::Presenters::BaseDSL#internationalised DSL method can be called within your block harmlessly, but it has no side effects. Any resource interface that can take internationalised data for creation (or modification) must already have an internationalised representation, so the standard resources in the Hoodoo::Data::Resources collection will already have declared that internationalisation applies.
Example 1:
to_create do
string :name, :length => 32, :required => true
text :description
end
Example 2: With a resource
to_create do
resource Product # Fields are *inline*
end
- &block
-
Block, passed to Hoodoo::Presenters::Object, describing the fields used for resource creation.
Source: show
# File lib/hoodoo/services/services/interface.rb, line 557 def to_create( &block ) obj = Class.new( Hoodoo::Presenters::Base ) obj.schema( &block ) self.class.send( :to_create=, obj ) end
Specify parameters related to common index parameters. The block contains calls to the DSL described by Hoodoo::Services::Interface::ToListDSL. The default values should be described by your platform's API - hard-coded at the time of writing as:
limit 50
sort :created_at => [ :desc, :asc ]
search nil
filter nil
Source: show
# File lib/hoodoo/services/services/interface.rb, line 510 def to_list( &block ) Hoodoo::Services::Interface::ToListDSL.new( self.class.instance_variable_get( '@to_list' ), &block ) end
As to_create, but applies when modifying existing resource instances. To avoid repeating yourself, if your modification and creation parameter requirements are identical, call update_same_as_create.
The “required” flag is ignored for updates, because an omitted field for an update to an existing resource instance simply means “do not change the current value”. As with to_create, default values have relevance to the rendering stage only and have no effect here.
- &block
-
Block, passed to Hoodoo::Presenters::Object, describing the fields used for resource modification.
Source: show
# File lib/hoodoo/services/services/interface.rb, line 576 def to_update( &block ) obj = Class.new( Hoodoo::Presenters::Base ) obj.schema( &block ) # When updating, 'required' fields in schema aren't required; you just # omit a field to avoid changing its value. Walk the to-update schema # graph stripping out any such problematic attributes. # obj.walk do | property | property.required = false end self.class.send( :to_update=, obj ) end
Declares that the expected JSON fields described in a to_create call are the same as those required for modifying resources too.
Example:
update_same_as_create
…and that's all. There are no parameters or blocks needed.
Source: show
# File lib/hoodoo/services/services/interface.rb, line 600 def update_same_as_create self.send( :to_update, & self.class.to_create().get_schema_definition() ) end
Declare the major version of the interface being implemented. All
service endpoints appear at “/v{version}/{endpoint}” relative to whatever
root an edge layer defines. If a service interface does not specifiy its
version, 1
is assumed.
Two interfaces can exist on the same endpoint provided their versions are different since the resulting route to reach them will be different too.
version
-
Integer major version number, e.g
2
.
Source: show
# File lib/hoodoo/services/services/interface.rb, line 355 def version( major_version ) self.class.send( :version=, major_version.to_s.to_i ) end