Tutorial - Capturing data from service calls

What you’ll learn

  1. How Granitic captures data from HTTP web service calls and binds it to your application’s data structures

Prerequisites

  1. Follow the Granitic installation instructions
  2. Read the before you start tutorial
  3. Either have completed tutorial 3 or open a terminal and run
cd $GOPATH/src/github.com/graniticio
git clone https://github.com/graniticio/granitic-examples.git
cd $GOPATH/src/github.com/graniticio/granitic-examples/tutorial
./prepare-tutorial.sh 4

Data capture

Granitic’s core purpose is to streamline the process of building web and micro services in Go. One of the most time consuming aspects of buliding web services is writing code to read inbound data from an HTTP request and binding that data to your application’s data structures. This tutorial explains how Granitic makes this easier (the next tutorial will cover the validation of the data you capture).

Web services that are provided as HTTP endpoints generally allow callers to supply data in three different parts of the HTTP request:

  1. The HTTP request’s body
  2. The path component of the URL
  3. The query component of the URL

or some combination of the above. Granitic automates the process of extracting these three types of data from the request and mapping the data to fields on your application’s data structures.

Web service design patterns

Granitic is agnostic as to which web service design pattern (REST, RPC etc) your application follows. The majority of the examples in these tutorials are ‘REST-like’, but that is not to suggest that Granitic favours REST over other patterns.

Meta-data

Callers of HTTP web services may also supply meta-data about the request as request headers. Granitic gives you application access to these headers but does not currently offer a way of automatically binding those headers to your data structures.

Path and query binding

Our tutorial application already supports a single GET endpoint to allow us to recover the details of a recording artist. Start your tutorial application:

cd $GOPATH/src/granitic-tutorial/recordstore
grnc-bind
go build
./recordstore -c resource/config,resource/env/production.json

and visit http://localhost:8080/artist to see what happens when you execute a GET request.

We want to allow the caller of this endpoint to specify which artist they’d like details for. Open endpoint/artist.go and add the following struct to the end of the file.

type ArtistRequest struct {
  Id int
  NormaliseName *types.NilableBool
}

Make sure you add github.com/graniticio/granitic/types to that file’s list of imports if your IDE hasn’t already done it for you.

Nilable types

A side-effect of Go’s system of zero values for variables is that it can make recovering data from web service calls ambiguous. For example, if you accept a boolean via a query parameter and the value of the boolean is false, how does your code know if it’s false because:

  • The caller explictly set the value to false or
  • The caller didn’t supply that parameter at all, so the variable just defaulted to false.

Granitic’s soltuion this problem is to provide a set of ‘nilable’ struct versions of primitive types (see the Godoc) that provide additional methods to indicate whether the value was explicity set by the caller or was an automatic zero value.

Configuring path binding

A common REST-like technique is to allow a caller to specify the ID of the required resource (in this case a recording artist) into the path of the request. E.g. /artist/1234. We will configure Granitic to extract that ID and inject it into the Id field of the ArtistRequest struct you defined above.

All of the automated tasks associated with a Granitic web service endpoint are handled by the handler.WsHandler struct.

If you open the file resource/components/components.json you will see:

"artistHandler": {
  "type": "handler.WsHandler",
  "HttpMethod": "GET",
  "Logic": "ref:artistLogic",
  "PathPattern": "^/artist"
}

The component artistHandler is an instance of handler.WsHandler and we can define how path binding will work through configuration. Change the definition of your artistHandler component so it looks like:

"artistHandler": {
  "type": "handler.WsHandler",
  "HttpMethod": "GET",
  "Logic": "ref:artistLogic",
  "PathPattern": "^/artist/([\\d]+)[/]?$",
  "BindPathParams": ["Id"]
}

We’ve altered the regular expression that this endpoint expects to

^/artist/([\d]+)[/]?$

so that in order to match an incoming request, the request path must include a number and an optional trailing slash. We’ve also defined a regular expression group around the part of the path that will be considered as representing the requested ID.

We’ve also added a new field BindPathParams and set it to an array of strings. The number of strings in this array should match the number of groups in the PathPattern regex. Here we are saying that the value of the first regex group should be injected into a field called ‘Id’.

The last step is to tell Grantic that the Id field we’re refering to is the one on our ArtistRequest struct. This is done in code by making your ArtistLogic struct implement handler.WsUnmarshallTarget

The method

UnmarshallTarget() interface{}

required by this interface allows each endpoint to create a ‘target’ object that any data from a request will be decanted into.

Change your ArtistLogic struct to look like:

type ArtistLogic struct {
  EnvLabel string
  Log      logging.Logger
}

func (al *ArtistLogic) Process(ctx context.Context, req *ws.WsRequest, res *ws.WsResponse) {

  ar := req.RequestBody.(*ArtistRequest)

  a := new(ArtistDetail)
  a.Name = "Some Artist"

  res.Body = a

  l := al.Log
  l.LogTracef("Request for artist with ID %d", ar.Id)

}

func (al *ArtistLogic) UnmarshallTarget() interface{} {
  return new(ArtistRequest)
}

Stop, rebuild and restart your application:

grnc-bind && go build && ./recordstore -c resource/config,resource/env/production.json

Visiting http://localhost:8080/artist will now result in a 404 Not Found error, but visiting http://localhost:8080/artist/1234 should result in a response and a log line similar to:

09/Oct/2017:12:44:13 Z TRACE [artistLogic] Request for artist with ID 1234

Type assertion

To work flexibly with any custom code you might create, the various methods and interfaces in Granitic’s handler package tend to work with interface{} types. One of the side effects of this is that the code in your endpoint’s Process method will need to perform a type assertion when accessing the contents of a request’s body. In this case the line

ar := req.RequestBody.(*ArtistRequest)

performs the required check.

Binding query parameters

The technique for binding query parameters to your ‘target’ object is very similar to that used for binding path parameters. Edit your components.json file and add the following to the definition of your artistHandler component:

   "FieldQueryParam": {
      "NormaliseName": "normalise"
   }

and add the following Go to the end your ArtistLogic.Process method:

  if ar.NormaliseName != nil && ar.NormaliseName.Bool() {
    a.Name = strings.ToUpper(a.Name)
  }

Rebuild and restart your application. Visiting http://localhost:8080/artist/1234?normalise=true will now cause the returned artist’s name to be capitalised.

Extracting data from the request body

Path parameters and query parameters are only useful for submitting limited amounts of semi-structured data to a web service. More common is to use a POST or PUT request to include more complex data in the body of an HTTP request. Granitic has built-in support for accepting data in an HTTP request body as JSON or XML. The following examples all use JSON, refer to the facility/ws and ws/xml GoDoc to discover how to use XML instead.

We will create a new endpoint to accept details of a new artist as POSTed JSON. Add the following to your artist.go file:

type SubmitArtistLogic struct {
  Log      logging.Logger
}

func (sal *SubmitArtistLogic) Process(ctx context.Context, req *ws.WsRequest, res *ws.WsResponse) {

  sar := req.RequestBody.(*SubmittedArtistRequest)

  sal.Log.LogInfof("New artist %s", sar.Name)

  //Hardcoded 'ID' of newly created artist - just a placeholder
  res.Body = struct {
   Id int
  }{0} 

}

func (sal *SubmitArtistLogic) UnmarshallTarget() interface{} {
  return new(SubmittedArtistRequest)
}


type SubmittedArtistRequest struct {
  Name string
  FirstYearActive *types.NilableInt64
}

This defines new endpoint logic that will expect a populated SubmittedArtistRequest to be supplied as the ws.WsRequest.RequestBody.

In order to have this code invoked, we will need to add the following to the components map in our components.json file:

"submitArtistLogic": {
  "type": "endpoint.SubmitArtistLogic"
},

"submitArtistHandler": {
  "type": "handler.WsHandler",
  "HttpMethod": "POST",
  "Logic": "ref:submitArtistLogic",
  "PathPattern": "^/artist[/]?$"
}

Check your config.json file and make sure the GlobalLogLevel is set to INFO

grnc-bind && go build && ./recordstore -c resource/config,resource/env/production.json

Testing POST services

Testing POST and PUT services is more complex than GET services as browsers don’t generally have built-in mechanisms for setting the body of a request. There are several browser extensions available that facilitate this sort of testing. The following instructions are based on Advanced Rest Client (ARC) for Chrome

POST a new artist

  1. Open ARC
  2. Set ‘Request URL’ to http://localhost:8080/artist
  3. Select the ‘POST’ radio button
  4. From the ‘Custom content type’ picklist choose application/json
  5. Enter the ‘test JSON’ below into the large text area at the bottom of the page
  6. Press SEND
  7. You should receive a JSON formatted response with an Id of 0 and see a log line similar to: 09/Oct/2017:14:11:15 Z INFO [submitArtistLogic] New artist Another Artist

Test JSON

{
  "Name": "Another Artist",
  "FirstYearActive": 2010
}

Recap

  • Granitic can extract data from the path, query and body of an HTTP request and bind it to your custom Go structs.
  • All this behaviour is configurable by changing the configuration of your handler components
  • Handler components are instances of handler.WsHandler

Further reading

Next

The next tutorial covers the validation of data submitted to web services