In a previous installment we built a simple JSON service to add a store to our workspace. In this article we will extend this further by also adding a service that lists existing stores and returns the list as JSON objects.
To kick this off, this is the end result (output reformatted slightly for readability):
$ curl -X POST -H "Accept: application/json" http://localhost:8080/stores/list
{"stores": [{"name": "Blue Mango","city": "Fruity Town"},
{"name": "Green Banana","city": "Fruity Town"},
{"name": "Scoops Downtown","city": "Creamy Town"},
{"name": "Scoops Eastside","city": "Creamy Town"},
{"name": "Scoops Westside","city": "Creamy Town"},
{"name": "Yellow Berry","city": "Fruity Town"}]}
Like last time, we’ll build our service in 4 steps:
- Define the service request and response formats
- Define the service logic
- Map the service to a URL
- Test our service
The full source code to this article is availble from Bitbucket.
Define service request and response formats
We extend our existing stores.proto
file with three more definitions:
//lang:protobuf
message StoreListRequest {
}
message StoreListResponse {
repeated Store stores = 1 [(blox.options.set) = true];
}
message Store {
required string name = 1;
required string city = 2;
}
The first message describes the request message. Since no request parameters are required, this message is empty. The StoreListResponse
message describes our service response. We want it to return just one thing: a list of stores. For this we use a repeated
property named stores
. Since we’re not really interested in the order of the result, and because our results will always be a set (and to make our life easier writing the logic later on), we add a blox.options.set
attribute to this repeated
property. This marks stores
as a set.
We want to return more information about a store than just a name (in which case a string
property would have sufficed). For this reason we introduce an auxiliary message named Store
that contains all the properties we want to return, in this case just two: name
and city
.
To compile the protocol buffers run:
$ lb config
$ make
The deeply burried build/sepcom/project/proto-gen/stores.module/stores.logic
files is now updated with our new messages. Here are the relevant parts:
//lang:logiql
StoreListRequest(_) -> .StoreListRequestConstructor[i]=x ->int(i), StoreListRequest(x).
StoreListResponse(_) -> .StoreListResponseConstructor[i]=x ->int(i), StoreListResponse(x).
StoreListResponse_stores(x, y) -> StoreListResponse(x), stores:Store(y).
Store(_) -> .StoreConstructor[i]=x ->int(i), Store(x).
Store_name[x] = y -> Store(x), string(y).
Store_city[x] = y -> Store(x), string(y).
Nothing too surprising here. The one thing of note is StoreListResponse_stores
, which is the LogiQL version of our repeated Store
property. This predicate suggests that to return multiple stores, we create Store
entities and pulse StoreListResponse_stores
facts for each where the first argument is the response and the second the Store
entity.
Time to write the logic to implement the service based on these definitions.
Service logic
For our service logic we’ll create a new services/list_stores.logic
file. As before, we’ll use an answer
predicate to track requests and responses and a rule to ensure that every request results receives a response:
//lang:logiql
answer[req] = resp -> stores:StoreListRequest(req), stores:StoreListResponse(resp).
lang:constructor(`answer).
lang:pulse(`answer).
+stores:StoreListResponse(resp),
+answer[req] = resp <-
+stores:StoreListRequest(req)
In our code we will have to map our internal representation of locations
//lang:logiql
city(c) -> .
city_by_name[name] = city -> city(city), string(name).
store(s) -> .
store_by_name[name] = store -> store(store), string(name).
store_in_city[s] = c -> store(s), city(c).
lang:constructor(`city_by_name).
lang:constructor(`store_by_name).
Into the protobuf representation. To do so, we use another auxiliary predicate in our service logic file (the location:*
predicates are aliases for our internal predicates and stores:*
are aliases to the LogiQL predicates generated from our protobufs):
//lang:logiql
store2proto[store] = proto -> location:store(store), stores:Store(proto).
lang:constructor(`store2proto).
lang:pulse(`store2proto).
Alright. So, what do we want to happen when a StoreListRequest
comes in? Well, we need to create a protobuf-compatible version of all existing store
entities and map the translation in store2proto
:
//lang:logiql
+stores:Store(proto),
+stores:Store_name[proto] = storeName,
+stores:Store_city[proto] = storeCity,
+store2proto[store] = proto <-
+stores:StoreListRequest(_),
location:store(store),
location:store_by_name[storeName] = store,
location:store_in_city[store] = city,
location:city_by_name[storeCity] = city.
But now we just have Store
protobuf entities “floating” in the store2proto
predicate. To attach them to the StoreListResponse
, we have to pulse them into the StoreListResponse_stores
predicate.
Luckily, that’s not hard:
//lang:logiql
+stores:StoreListResponse_stores(resp, proto) <-
+stores:StoreListResponse(resp),
+store2proto[_] = proto.
And that’s it! Service logic done.
Map the service to a URI
We can now map our service to a URI by extending our existing service_config.logic
file:
//lang:logiql
service_by_prefix["/stores/list"] = x,
default_protobuf_service(x) {
protobuf_protocol[] = "stores",
protobuf_request_message[] = "StoreListRequest",
protobuf_response_message[] = "StoreListResponse"
}.
Done.
Time to test! To compile and run our application, run the following commands in a shell:
$ lb config
$ make start-services
When successful, run the curl:
$ curl -X POST -H "Accept: application/json" http://localhost:8080/stores/list
{"stores": [{"name": "Blue Mango","city": "Fruity Town"},
{"name": "Green Banana","city": "Fruity Town"},
{"name": "Scoops Downtown","city": "Creamy Town"},
{"name": "Scoops Eastside","city": "Creamy Town"},
{"name": "Scoops Westside","city": "Creamy Town"},
{"name": "Yellow Berry","city": "Fruity Town"}]}
Add a new store with our other service, and run our service again to check if it was added:
$ curl -X POST -H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{"name": "My Store", "city": "My City"}' \
http://localhost:8080/stores/add
{"message": "Added"}
$ curl -X POST -H "Accept: application/json" http://localhost:8080/stores/list
{"stores": [{"name": "Scoops Eastside","city": "Creamy Town"},
{"name": "Blue Mango","city": "Fruity Town"},
{"name": "Yellow Berry","city": "Fruity Town"},
{"name": "Scoops Downtown","city": "Creamy Town"},
{"name": "Green Banana","city": "Fruity Town"},
{"name": "My Store","city": "My City"},
{"name": "Scoops Westside","city": "Creamy Town"}]}
And there have it!
For more in-depth information on creating protobuf services, read the chapter on our reference manual.