Class: Hoodoo::Services::Interface

Inherits:
Object
  • Object
show all
Defined in:
lib/hoodoo/services/services/interface.rb

Overview

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.

Defined Under Namespace

Classes: ToList, ToListDSL

Class Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Class Attribute Details

.actionsObject

Supported action methods as a Set of symbols with one or more of :list, :show, :create, :update or :delete. The presence of a Symbol indicates a supported action. If empty, no actions are supported. The default is for all actions to be present in the Set.



843
844
845
# File 'lib/hoodoo/services/services/interface.rb', line 843

def actions
  @actions
end

.additional_permissionsObject

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.



913
914
915
# File 'lib/hoodoo/services/services/interface.rb', line 913

def additional_permissions
  @additional_permissions
end

.embedsObject

Array of strings listing allowed embeddable things. Each string matches the split up comma-separated value for query string _embed or _reference keys. For example:

...&_embed=foo,bar

…would be valid provided there was an embedding declaration such as:

embeds :foo, :bar

…which would in turn lead this accessor to return:

[ 'foo', 'bar' ]


873
874
875
# File 'lib/hoodoo/services/services/interface.rb', line 873

def embeds
  @embeds
end

.endpointObject

Endpoint path as declared by service, without preceding “/”, possibly as a symbol - e.g. :products for “/products” as an implied endpoint.



820
821
822
# File 'lib/hoodoo/services/services/interface.rb', line 820

def endpoint
  @endpoint
end

.errors_forObject

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.



905
906
907
# File 'lib/hoodoo/services/services/interface.rb', line 905

def errors_for
  @errors_for
end

.implementationObject

Implementation class for the service. An Hoodoo::Services::Implementation subclass - the class, not an instance of it.



836
837
838
# File 'lib/hoodoo/services/services/interface.rb', line 836

def implementation
  @implementation
end

.public_actionsObject

Public action methods as a Set of symbols with one or more of :list, :show, :create, :update or :delete. The presence of a Symbol indicates an action open to the public and not subject to session security. If empty, all actions are protected by session security. The default is an empty Set.



851
852
853
# File 'lib/hoodoo/services/services/interface.rb', line 851

def public_actions
  @public_actions
end

.resourceObject

Name of the resource the interface addresses as a symbol, e.g. :Product.



830
831
832
# File 'lib/hoodoo/services/services/interface.rb', line 830

def resource
  @resource
end

.secure_log_forObject

Secure log actions set by #secure_log_for - see that call for details. The default is an empty Hash.



856
857
858
# File 'lib/hoodoo/services/services/interface.rb', line 856

def secure_log_for
  @secure_log_for
end

.to_createObject

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 nil, arbitrary data is acceptable (the implementation becomes entirely responsible for data validation).



890
891
892
# File 'lib/hoodoo/services/services/interface.rb', line 890

def to_create
  @to_create
end

.to_updateObject

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 nil, arbitrary data is acceptable (the implementation becomes entirely responsible for data validation).



898
899
900
# File 'lib/hoodoo/services/services/interface.rb', line 898

def to_update
  @to_update
end

.versionObject

Major version of interface as an integer. All service endpoint routes have “vversion/” as a prefix, e.g. “/v1/products”.



825
826
827
# File 'lib/hoodoo/services/services/interface.rb', line 825

def version
  @version
end

Class Method Details

.to_listObject

A Hoodoo::Services::Interface::ToList instance describing the list parameters for the interface as a Set of Strings. See also Hoodoo::Services::Interface::ToListDSL.



879
880
881
882
# File 'lib/hoodoo/services/services/interface.rb', line 879

def to_list
  @to_list ||= Hoodoo::Services::Interface::ToList.new
  @to_list
end

Instance Method Details

#actions(*supported_actions) ⇒ Object

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


373
374
375
376
377
378
379
380
381
382
# 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

#additional_permissions_for(action) {|p| ... } ⇒ Object

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.

Yields:

  • (p)


726
727
728
729
730
731
732
733
734
735
736
737
738
739
# 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

#embeds(*embeds) ⇒ Object

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


496
497
498
# File 'lib/hoodoo/services/services/interface.rb', line 496

def embeds( *embeds )
  self.class.send( :embeds=, embeds.map { | item | item.to_s } )
end

#endpoint(uri_path_fragment, implementation_class) ⇒ Object

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.



333
334
335
336
337
338
339
340
341
342
343
# 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

#errors_for(domain, &block) ⇒ Object

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


623
624
625
626
627
628
629
630
631
# 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

#public_actions(*public_actions) ⇒ Object

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.



402
403
404
405
406
407
408
409
410
411
# 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

#secure_log_for(secure_log_actions = {}) ⇒ Object

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.



462
463
464
465
466
467
468
469
470
471
# 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

#to_create(&block) ⇒ Object

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::Object#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.



557
558
559
560
561
562
# 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

#to_list(&block) ⇒ Object

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


510
511
512
513
514
515
# 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

#to_update(&block) ⇒ Object

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.



576
577
578
579
580
581
582
583
584
585
586
587
588
589
# 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

#update_same_as_createObject

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.



600
601
602
# 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

#version(major_version) ⇒ Object

Declare the major version of the interface being implemented. All service endpoints appear at “/vversion/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.



355
356
357
# File 'lib/hoodoo/services/services/interface.rb', line 355

def version( major_version )
  self.class.send( :version=, major_version.to_s.to_i )
end