Skip to content

Dynamic endpoints

Derek Clarkson edited this page Nov 21, 2022 · 4 revisions

Dynamic endpoints are designed to provide just enough flexibility to simulate what a real server does without adding too much complexity. They are not designed for complex or sophisticated logic, merely as a vehicle for making basic decisions on how to respond to a given request.

Essentially a dynamic endpoint is where a function or closure is defined as the response instead of a fixed status and payload. The function/closure is then called to return the actual response when an incoming request is matched.

XCTest

In a Swift XCTest setup dynamic endpoints are handled by closures whose signature is

HTTPResponse.dynamic(_ handler: (HTTPRequest, Cache) async -> HTTPResponse)

For example:

HTTPEndpoint(.GET, "/config/:version", .dynamic { request, cache in
    request.pathParameters.version == 1.0 ? .ok() : .badRequest
}) 

YAML

In a YAML config dynamic endpoints are handled by a javascript function. This function can be 'in-line' in the YAML or stored in a seperate file.

Either way the function must called response(...) and look like this:

function response(Request, cache) {
    // return the response
}

And take two arguments. A request and a cache. For example:

function response(request, cache) {
    return request.pathParameters.version == 1.0 ? Response.ok() : Response.badRequest();
}

Done 'in-line':

http:
  api: get /config/:version
  javascript: |
    function response(request, cache) {
      return request.pathParameters.version == 1.0 ? Response.ok() : Response.badRequest();
    }

And if we had the function stored in the file GetConfig.js:

http:
  api: get /config/:version
  javascriptFile: GetConfig.js

The Request argument

The request argument passed to the closure and javascript function is a thin wrapper around the incoming request designed to provide access to the incoming request data as well as a variety of Voodoo specific deserialisation functions.

Standard properties

The first set of properties are the standard HTTP request properties you would expect to find on any incoming request object.

Property iOS type Javascript type Description
method HTTPMethod String The HTTP method.
headers [String:String] Object The incoming request headers.
In iOS this is tagged with @dynamicMemberLookup so header values can be accessed using the key as a property name.
In Javascript this is an object with all the header values as properties. Note that if there are duplicate headers, their values will be grouped into an array.
path String? String The path part of the incoming URL. ie. /accounts/user.
query String String The untouched query string from the incoming URL. For example ?firstname=Derek&lastname=Clarkson.
body Data? String The raw body of the incoming request.

Voodoo properties

After the standard properties, Voodoo then add a range of convenience properties that perform commonly applied transforms.

Property iOS type Javascript type Description
pathComponents [String] Array The path split up into it's components. ie. /accounts/user would be split into the array ["/", "accounts", "user"]. This is most useful when functionality is layered in RESTful style APIs and you want to make decisions based on various path components. Note that the first component (/) is the root indicator as opposed to the component separator used for the rest of a path.
pathParameters [String:String] Object Any parameters extracted from the path. For example, if the endpoint path is /accounts/:user and an incoming request has the path /accounts/1234 then this contains the key value "user":"1234".
In iOS this is tagged with @dynamicMemberLookup the parameter values can be accessed using the key as a property names.
In Javascript this is an object with all the parameters values as properties.
queryParameters [String:String]? Object The extracted query parameters. For example ?firstname=Derek&lastname=Clarkson would result in the key value pairs "firstname":"Derek" and "lastname":"Clarkson". Note that it is possible for query parameters to repeat with different values. For example ?search=books&search=airplanes. If found, duplicates are mapped into a single array under the one key with all the value in the order specified. ie. "search":["books","airplanes"].
In iOS this is tagged with @dynamicMemberLookup the parameter values can be accessed using the key as a property names.
In Javascript this is an object with all the parameters values as properties.
bodyJSON Any? Object If the Content-Type header specifies a payload of JSON, this will attempt to deserialise the body and return the resulting object. That object will usually be either a single value, array or a dictionary/map.
bodyYAML Any? Object If the Content-Type header specifies a payload of YAML, this will attempt to deserialise the body and return the resulting object. That object will usually be either a single value, array or a dictionary/map.
formParameters [String:String] Object If the Content-Type header specifies that the body contains form data, this will attempt to deserialise it into a dictionary/map and return it.
graphQLRequest GraphQLRequest? Object If the incoming request matches the specification for a GraphQL request then this will deserialise that request and return it.

Voodoo iOS functions

In addition to the above properties, iOS closures can also access some extra functions.

Function Description
contentType(is contentType: String) -> Bool Returns true if the request has the specified content-type header.
decodeBodyJSON<T>(as type: T.Type) -> T? where T: Decodable If the content-type header is application/json and the body can be decoded as the requested type, then it is returned.

The Cache argument

The second argument passed to the closure/function is a reference to the pan-request cache.

The pan-request cache is a simple in-memory cache that is created when the server is started and wiped when it is stopped. It's sole purpose is to act as a storage area where simple pieces of data can be stored so that they can be accessed by multiple request.

An example of this might be for a login API to store the name of the user in the cache so that a request for home page data can inject the user's name into the response JSON.

The cache object itself is a very simple map/dictionary like object that stores key/value pairs. In Swift it is also @dynamicMemberLookup coded for convenience.

Here is a simple example of using the cache in a Swift setup:

// Store the user name from a login request.
server.add(.POST, "/login") { request, cache in
    cache.username = request.formParameters.name
    return .ok()
}

// Return the user name.
server.add(.GET, "/profile") { _, cache in
    return .ok(body: json(["username": cache.username])}
}

Clone this wiki locally