Creating Protobuf/JSON Services Part 2

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:

  1. Define the service request and response formats
  2. Define the service logic
  3. Map the service to a URL
  4. 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.

0 Comments

Leave a reply

© Copyright 2023. Infor. All rights reserved.

Log in with your credentials

Forgot your details?