Step 4. Designing an HTTP Client

The standard way of communicating with a microservice is via the HTTP protocol. It allows calling microservices that work on a separate server, or in their own container. Our example microservice uses a simplified version of the HTTP protocol that is automatically generated using the Commandable pattern.

Then, creates a new class for the Commandable REST client and an implementation for each of the microservice’s methods. This is done by calling the REST API’s methods using the methods of the parent Commandable REST client, passing the necessary set of parameters, and then processing the response’s result. Since the answer from the client is returned as JSON, some programming languages will require that you first convert it to an instance with a specific type. Be sure to remember this when writing your HTTP clients.

The client’s resulting code is listed below:

/version1/BeaconsHttpClientV1.ts

package clients1

import (
	"context"

	data1 "github.com/pip-services-samples/service-beacons-go/data/version1"
	cdata "github.com/pip-services4/pip-services4-go/pip-services4-commons-go/data"
	cquery "github.com/pip-services4/pip-services4-go/pip-services4-data-go/query"
	cclients "github.com/pip-services4/pip-services4-go/pip-services4-http-go/clients"
	clients "github.com/pip-services4/pip-services4-go/pip-services4-http-go/clients"
)

type BeaconsHttpClientV1 struct {
	*cclients.CommandableHttpClient
}

func NewBeaconsHttpClientV1() *BeaconsHttpClientV1 {
	c := &BeaconsHttpClientV1{
		CommandableHttpClient: cclients.NewCommandableHttpClient("v1/beacons"),
	}
	return c
}

func (c *BeaconsHttpClientV1) GetBeacons(ctx context.Context,
	filter cquery.FilterParams,
	paging cquery.PagingParams) (*cquery.DataPage[data1.BeaconV1], error) {

	params := cdata.NewEmptyStringValueMap()
	c.AddFilterParams(params, &filter)
	c.AddPagingParams(params, &paging)

	response, err := c.CallCommand(ctx, "get_beacons", cdata.NewAnyValueMapFromValue(params.Value()))

	if err != nil {
		return cquery.NewEmptyDataPage[data1.BeaconV1](), err
	}

	return clients.HandleHttpResponse[*cquery.DataPage[data1.BeaconV1]](response, "")
}

func (c *BeaconsHttpClientV1) GetBeaconById(ctx context.Context,
	beaconId string) (*data1.BeaconV1, error) {
	params := cdata.NewAnyValueMapFromTuples(
		"beacon_id", beaconId,
	)

	response, err := c.CallCommand(ctx, "get_beacon_by_id", params)

	if err != nil {
		return nil, err
	}

	return clients.HandleHttpResponse[*data1.BeaconV1](response, "")
}

func (c *BeaconsHttpClientV1) GetBeaconByUdi(ctx context.Context,
	udi string) (*data1.BeaconV1, error) {
	params := cdata.NewAnyValueMapFromTuples(
		"udi", udi,
	)

	response, err := c.CallCommand(ctx, "get_beacon_by_udi", params)
	if err != nil {
		return nil, err
	}

	return clients.HandleHttpResponse[*data1.BeaconV1](response, "")
}

func (c *BeaconsHttpClientV1) CalculatePosition(ctx context.Context,
	siteId string, udis []string) (*data1.GeoPointV1, error) {
	params := cdata.NewAnyValueMapFromTuples(
		"site_id", siteId,
		"udis", udis,
	)

	response, err := c.CallCommand(ctx, "calculate_position", params)
	if err != nil {
		return nil, err
	}

	return clients.HandleHttpResponse[*data1.GeoPointV1](response, "")
}

func (c *BeaconsHttpClientV1) CreateBeacon(ctx context.Context,
	beacon data1.BeaconV1) (*data1.BeaconV1, error) {
	params := cdata.NewAnyValueMapFromTuples(
		"beacon", beacon,
	)

	response, err := c.CallCommand(ctx, "create_beacon", params)
	if err != nil {
		return nil, err
	}

	return clients.HandleHttpResponse[*data1.BeaconV1](response, "")
}

func (c *BeaconsHttpClientV1) UpdateBeacon(ctx context.Context,
	beacon data1.BeaconV1) (*data1.BeaconV1, error) {
	params := cdata.NewAnyValueMapFromTuples(
		"beacon", beacon,
	)

	response, err := c.CallCommand(ctx, "update_beacon", params)
	if err != nil {
		return nil, err
	}

	return clients.HandleHttpResponse[*data1.BeaconV1](response, "")
}

func (c *BeaconsHttpClientV1) DeleteBeaconById(ctx context.Context,
	beaconId string) (*data1.BeaconV1, error) {
	params := cdata.NewAnyValueMapFromTuples(
		"beacon_id", beaconId,
	)

	response, err := c.CallCommand(ctx, "delete_beacon_by_id", params)
	if err != nil {
		return nil, err
	}

	return clients.HandleHttpResponse[*data1.BeaconV1](response, "")
}

/src/version1/BeaconsHttpClientV1.py

from typing import Optional, List, Any

from pip_services4_data.query import DataPage, FilterParams, PagingParams
from pip_services4_http.clients import CommandableHttpClient

from .IBeaconsClientV1 import IBeaconsClientV1
from ...data.version1 import BeaconV1


class BeaconsHttpClientV1(CommandableHttpClient, IBeaconsClientV1):
    def __init__(self):
        super(BeaconsHttpClientV1, self).__init__("v1/beacons")

    def get_beacons_by_filter(self, context: Optional[IContext], filter: FilterParams,
                              paging: PagingParams) -> DataPage:
        result = self.call_command(
            'get_beacons',
            context,
            {
                'filter': filter,
                'paging': paging
            }
        )
        return DataPage(result['data'], result['total'])

    def get_beacon_by_id(self, context: Optional[IContext], id: str) -> dict:
        return self.call_command(
            'get_beacon_by_id',
            context,
            {
                'id': id
            }
        )

    def get_beacon_by_udi(self, context: Optional[IContext], udi: str) -> dict:
        return self.call_command(
            'get_beacon_by_udi',
            context,
            {
                'udi': udi
            }
        )

    def calculate_position(self, context: Optional[IContext], site_id: str, udis: List[str]) -> Any:
        return self.call_command(
            'calculate_position',
            context,
            {
                'site_id': site_id,
                'udis': udis
            }
        )

    def create_beacon(self, context: Optional[IContext], entity: BeaconV1) -> dict:
        return self.call_command(
            'create_beacon',
            context,
            {
                'beacon': entity
            }
        )

    def update_beacon(self, context: Optional[IContext], entity: BeaconV1) -> dict:
        return self.call_command(
            'update_beacon',
            context,
            {
                'beacon': entity
            }
        )

    def delete_beacon_by_id(self, context: Optional[IContext], id: str) -> dict:
        return self.call_command(
            'delete_beacon_by_id',
            context,
            {
                'id': id
            }
        )

Not available

To be sure that our code works as intended, we should perform some functional testing. Test the Commandable HTTP REST client using the class with tests that we developed in the previous step. To do this, create an instance of the HTTP REST client and pass it as a parameter to our set of tests. An example implementation of the tests can be found in the example’s:

All tests should pass successfully.This finishes the development of our clients. As a result, we ended up with 2 clients: one for working from within a monolithic application, and another for working with the microservice from a different application, when utilizing a distributed architecture.

To simulate the service, let’s create a test client in Step 5. Implementing a Mock Client.

Step 5. Implementing a Mock Client.