-
Notifications
You must be signed in to change notification settings - Fork 0
Dynamic endpoints
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.
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
}) 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.jsThe 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.
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. |
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. |
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 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])}
}