callapi

package
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 17, 2024 License: MPL-2.0 Imports: 6 Imported by: 0

Documentation

Overview

Package callapi provides simple way to call remote api server which is written with jsonapi package.

For calling an API server written with pakcage jsonapi, EP and NewEP should solve your problem.

For more complicated case, like signing the request, implementing your own Endpoint or using Builder should solve your problem.

Though not recommended, it's possible to call other APIs (Twitter, GCP, ...) by providing specially designed Encoder and Parser, or use Builder.

Example
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.

package main

import (
	"context"
	"fmt"
	"net/http"
	"net/http/httptest"
	"time"

	"github.com/raohwork/jsonapi"
)

// ParamGreeting represents parameters of Greeting API
type ParamGreeting struct {
	Name    string
	Surname string
}

// RespGreeting represents returned type of Greeting API
type RespGreeting struct {
	Name    string
	Surname string
	Greeted bool
}

// greeting is handler of Greeting API
func Greeting(r jsonapi.Request) (interface{}, error) {
	var p ParamGreeting
	if err := r.Decode(&p); err != nil {
		return nil, jsonapi.APPERR.SetData(
			"parameter format error",
		).SetCode("EParamFormat")
	}

	return RespGreeting{
		Name:    p.Name,
		Surname: p.Surname,
		Greeted: true,
	}, nil
}

// RunAPIServer creates and runs an API server
func RunAPIServer() *httptest.Server {
	http.Handle("/greeting", jsonapi.Handler(Greeting))
	return httptest.NewServer(http.DefaultServeMux)
}

func main() {
	// start the API server
	server := RunAPIServer()
	defer server.Close()

	caller := EP("POST", server.URL+"/greeting")

	// http request timeout info
	ctx, cancel := context.WithTimeout(context.TODO(), time.Second)
	defer cancel()

	var resp RespGreeting
	err := caller.Call(ctx, ParamGreeting{Name: "John", Surname: "Doe"}, &resp)
	if err != nil {
		fmt.Println(err)
		return
	}

	fmt.Printf(
		"Have we greeted to %s %s? %v",
		resp.Name, resp.Surname, resp.Greeted,
	)

}
Output:

Have we greeted to John Doe? true
Example (CustomEncoder)
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.

package main

import (
	"encoding/json"
	"errors"
	"io"
	"net/http"
	"net/url"
)

func MyEncoder(v any) ([]byte, error) {
	switch x := v.(type) {
	case map[string][]string:
		return []byte(url.Values(x).Encode()), nil
	case map[string]string:
		val := url.Values{}
		for k, v := range x {
			val.Set(k, v)
		}
		return []byte(val.Encode()), nil
	}
	return nil, errors.New("unsupported type")
}

type myAPIResp struct {
	Status string          `json:"status"` // ok or fail
	Data   json.RawMessage `json:"data"`
	Error  string          `json:"error"`
}

func MyParser(resp *http.Response, result interface{}) (err error) {
	defer resp.Body.Close()
	defer io.Copy(io.Discard, resp.Body)

	var tmp myAPIResp
	err = json.NewDecoder(resp.Body).Decode(&tmp)
	if err != nil {
		return
	}

	if tmp.Status == "ok" {
		return json.Unmarshal(tmp.Data, result)
	}

	if tmp.Error != "" {
		return errors.New(tmp.Error)
	}

	return errors.New("unknown error returned from server")
}

func main() {
	// this endpoint accepts post form, and returns json
	ep := Encoder(MyEncoder).
		EP(http.MethodPost, "https://example.com/api/my_endpoint").
		SendBy(nil).
		ParseWith(MyParser)
	// var result MyResult
	// err = ep.Call(param, &result)

	_ = ep
}
Example (CustomEndpoint)
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.

package main

import (
	"bytes"
	"context"
	"crypto/hmac"
	"crypto/sha1"
	"encoding/hex"
	"io"
	"net/http"
)

// MyEP creates an Endpoint which signs the request with hmac-sha1.
func MyEP(method, url string) Endpoint {
	return func(ctx context.Context, param any) (req *http.Request, err error) {
		buf, err := SlowSortEncoder()(param)
		if err != nil {
			return
		}

		req, err = http.NewRequestWithContext(
			ctx, method, url, bytes.NewReader(buf),
		)
		if err != nil {
			return
		}

		signer := hmac.New(sha1.New, []byte("my secret key"))
		io.WriteString(signer, req.URL.Path)
		io.WriteString(signer, ",my-client-id,")
		signer.Write(buf)
		sig := signer.Sum(nil)
		req.Header.Set("MY-SIGNATURE", hex.EncodeToString(sig))
		return
	}
}

func Auth(req *http.Request) (*http.Request, error) {
	req.Header.Set("MY-AUTH-TOKEN", "my secret token")
	return req, nil
}

func main() {
	// simple endpoint with auth token in header
	ep := NewEP(http.MethodGet, "https://example.com/api/my_endpoint").
		With(Auth).DefaultCaller()
	// var result MyResult
	// err = ep.Call(param, &result)

	// customized endpoint, with additional hmac signature in header
	ep = MyEP(http.MethodPost, "https://example.com/api/my_endpoint").
		With(Auth).DefaultCaller()
	// var result MyResult
	// err = ep.Call(param, &result)

	_ = ep
}

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func DefaultParser

func DefaultParser(resp *http.Response, result interface{}) error

DefaultParser parses response of a jsonapi

If any io or json parsing error occurred, an EFormat is returned.

Types

type Builder

type Builder struct {
	// Maker is a function to create request. DefaultEncoder().EP is used if nil
	Maker func(method, url string) Endpoint
	// [http.DefaultClient] is used if nil
	Sender *http.Client
	// DefaultParser is used if nil
	Parser Parser
}

Builder is a builder to build many Caller with same settings. Zero value builds same Caller as EP does.

It is suggested to use Builder if any of following rules is matched:

  • There're too many endpoints; using Builder can save some key strokes.
  • Most of endpoints returns some data in response header.
  • Most of endpoints need dynamic value in header, message digest and etag for example.

func (Builder) EP

func (b Builder) EP(method, uri string) Caller

EP creates a Caller that uses b.Maker to create request, send the request by b.Sender and parse the response by b.Parser.

func (Builder) UseMaker

func (b Builder) UseMaker(m func(method, url string) Endpoint) Builder

UseMaker creates a new Builder that use m as maker.

func (Builder) UseParser

func (b Builder) UseParser(p Parser) Builder

UseParser creates a new Builder that use p as parser.

func (Builder) UseSender

func (b Builder) UseSender(cl *http.Client) Builder

UseSender creates a new Builder that use cl as sender.

type Caller

type Caller func(ctx context.Context, param, result any) error

Caller is a function calls to specific API endpoint.

In general, you build an Endpoint (using Encoder or implement by yourself) and create Caller by applying Sender and Parser to it:

var result MyAPIResultType
param := MyAPIParam{ ... }
err := endpoint.SendBy(sneder).ParseWith(parser).Call(ctx, param, result)

func EP

func EP(method, url string) Caller

EP creates a Caller from http method and url, with common settings which is suitable to use with package jsonapi:

If your api server requires more configurations, like passing auth token or hmac signature with http header, you can:

  • NewEP("POST", myApiUrl).With(aFunctionToSetHeader)
  • Write your own Endpoint

func (Caller) Call

func (c Caller) Call(ctx context.Context, param, result any) error

Call calls the API with Caller c. It's identical to c(ctx, param, result).

type EClient

type EClient struct {
	Origin error
}

EClient indicates this api call is failed due to client side error.

In general, Eclient represents errors occurred before sending request.

func (EClient) Error

func (e EClient) Error() string

func (EClient) Unwrap

func (e EClient) Unwrap() error

type EFormat

type EFormat struct {
	Origin error
}

EFormat indicates an error when parsing server response.

In general, EFormat represents errors occurred after request is sent.

func (EFormat) Error

func (e EFormat) Error() string

func (EFormat) Unwrap

func (e EFormat) Unwrap() error

type Encoder

type Encoder func(v any) ([]byte, error)

Encoder is a function which can encodes param into specific format.

func DefaultEncoder

func DefaultEncoder() Encoder

DefaultEncoder returns default encoder, which is simple json.Marshal.

func SlowSortEncoder

func SlowSortEncoder() Encoder

SlowSortEncoder is an Encoder which produces sorted json in super slow way.

It's not recommended to use in production.

func (Encoder) EP

func (e Encoder) EP(method, url string) Endpoint

EP creates an Endpoint which uses the Encoder to build request body.

type Endpoint

type Endpoint func(context.Context, any) (*http.Request, error)

Endpoint represents the spec of an API endpoint, a function that creates http request which fulfills all requirements the endpoint need.

func NewEP

func NewEP(method, url string) Endpoint

NewEP creates an Endpoint with default settings, see EP for detail.

func (Endpoint) DefaultCaller

func (ep Endpoint) DefaultCaller() Caller

DefaultCaller is shortcut to ep.SendBy(nil).ParseWith(DefaultParser)

func (Endpoint) SendBy

func (ep Endpoint) SendBy(cl *http.Client) Sender

SendBy creates a Sender which creates request using ep and send it by cl.

func (Endpoint) With

func (ep Endpoint) With(f func(*http.Request) (*http.Request, error)) Endpoint

With wraps ep by modifying the request with f.

type Parser

type Parser func(resp *http.Response, result interface{}) error

Parser parses server response and decode it. It takes responsibility to close response body.

type Sender

type Sender func(ctx context.Context, param any) (resp *http.Response, err error)

Sender is s function to send request to server.

func (Sender) ParseWith

func (s Sender) ParseWith(p Parser) Caller

ParseWith creates a Caller by parsing response returned by Sender s. It uses DefaultParser if p == nil.

type TypedCaller

type TypedCaller[I, O any] func(context.Context, I) (*O, error)

TypedCaller is type-safe Caller. Note that returned type is pointer.

It is designed to save some key strokes when writing API client method which supposed to return pointer type. For those methods returning value type:

func (c *MyClient) MyMethod(ctx context.Context, param MyParam) (ret MyResult, err error) {
	err = c.builder.EP("POST", c.host+"/my/method").Call(ctx, param, &ret)
	return
}
Example
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.

package main

import (
	"context"
	"net/http"
)

type SpecialImplParam struct{}
type SpecialImplResp string

type BadImplParam struct{}
type BadImplResp string

type GoodImplParam struct{}
type GoodImplResp string

func authWith(token, secret string) func(*http.Request) (*http.Request, error) {
	return func(r *http.Request) (*http.Request, error) {
		r.Header.Set("MY-API-TOKEN", token)
		r.Header.Set("MY-API-SECRET", secret)
		return r, nil
	}
}

func NewClient(host, token, secret string) *MyAPIClient {
	return &MyAPIClient{
		b: Builder{
			Maker: func(method, path string) Endpoint {
				return DefaultEncoder().
					EP(method, host+path).
					With(authWith(token, secret))
			},
		},
	}
}

type MyAPIClient struct {
	b Builder
}

// see how bad it can be without using [Builder] for repeative code.
func (c *MyAPIClient) SpecialImpl(ctx context.Context, param SpecialImplParam) (*SpecialImplResp, error) {
	// host, token, secret should be stored in MyAPIClient if not using Builder
	host, token, secret := "", "", ""

	var ret SpecialImplResp
	err := NewEP(http.MethodPost, host+"/spec/impl").
		With(authWith(token, secret)).
		DefaultCaller().
		Call(ctx, param, &ret)
	if err != nil {
		return nil, err
	}
	return &ret, nil
}

// imagine writing this code 20 times.....
func (c *MyAPIClient) BadImpl(ctx context.Context, param BadImplParam) (*BadImplResp, error) {
	var ret BadImplResp
	err := c.b.EP(http.MethodPost, "/bad/impl").Call(ctx, param, &ret)
	if err != nil {
		return nil, err
	}
	return &ret, nil
}

// saves few key strokes
func (c *MyAPIClient) GoodImpl(ctx context.Context, param GoodImplParam) (*GoodImplResp, error) {
	return Typed[GoodImplParam, GoodImplResp](
		c.b.EP(http.MethodPost, "/bad/impl"),
	).Call(ctx, param)

	// if this method uses different set of request headers
	// return Use[GoodImplParam, GoodImplResp](
	// 	c.b.UseMaker(anotherMaker).EP(http.MethodPost, "/bad/impl"),
	// ).Call(ctx, param)
}

func main() {
	// see methods of MyAPIClient for detail
	_ = NewClient("", "", "")
}

func Typed

func Typed[I, O any](c Caller) TypedCaller[I, O]

Typed creates TypedCaller from Caller.

func (TypedCaller[I, O]) Call

func (c TypedCaller[I, O]) Call(ctx context.Context, param I) (*O, error)

Call calls the API with TypedCaller c. It's identical to c(ctx, param).

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL