composing http endpoints
While I’ve been working on choclety, I’ve also been experimenting a bit. Choclety is just based on the observation that many times a return type of one endpoint (some representation of a resource) is an input to another. Relatedly I thought, wouldn’t it be fun to try composing endpoints, ie.
1
2
3
4
5
6
GET http://localhost:4567/a
# => {"a":"a"}
GET http://localhost:4567/a?compose=!b
# => {"b":"a + b"}
GET http://localhost:4567/a?compose=!b,c
# => {"c":"a + b + c"}
Here’s a working implementation in Ruby, using Sinatra:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
# prereqs:
# gem install sinatra
# gem install sinatra-contrib
require 'sinatra'
require 'json'
require 'sinatra/json'
require 'addressable/uri'
COMPOSITION_PREFIX = /([!\-+]?)(.*)/
COMPOSITION = {'+': 'PUT', '!': 'POST', '-': 'DELETE'}
COMPOSITION.default = 'GET'
COMPOSITION.freeze
helpers do
def compose(params, response, path)
prefix, path = COMPOSITION_PREFIX.match(path)[1], COMPOSITION_PREFIX.match(path)[2]
query_string = Addressable::URI.new(
query_values: response.merge(params)
).query
env_hash = {
'PATH_INFO' => "/#{path}",
'QUERY_STRING' => query_string,
'BODY' => response,
'REQUEST_METHOD' => COMPOSITION[prefix.to_sym]
}
p "composing #{path}"
call env.merge(env_hash)
end
def respond(response)
composition_stack = (params.delete('compose') || "").split(',')
if t = composition_stack.shift
params.merge!({ compose: composition_stack.join(',') })
status, headers, body = compose(params, response, t)
[status, headers, body]
else
json(response)
end
end
end
get '/a' do
p 'responding from a'
respond({a: "a"})
end
post '/b' do
a = params[:a]
p 'responding from b'
respond({b: "#{a} + b"})
end
get '/c' do
body = env['BODY']
p 'responding from c'
respond({c: ((body || {}).values + ['c']).join(' + ')})
end
The theoretical advantage to this is that you could collapse many requests in a single one. It’s dangerous though! In my example, you can compose via a GET and essentially force a POST to happen.
What’s more valuable about an example like this, to me, is simply that it surfaces an interesting constraint. If you force an API to be consistent across input types and return types, you can chain or compose calls provided you don’t need to parameterize the calls further.
You generally will need to parameterize API calls with user interaction. This emphasizes that integration point with your user, but also means you could write ‘adapters’ for other agents.