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 ¶
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 ¶
EP creates a Caller that uses b.Maker to create request, send the request by b.Sender and parse the response by b.Parser.
type Caller ¶
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 ¶
EP creates a Caller from http method and url, with common settings which is suitable to use with package jsonapi:
- If parameter is not nil, it is encoded by json.Marshal and content type is set to "application/json"
- request is sent by http.DefaultClient
- response is parsed by DefaultParser
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
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.
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.
type Encoder ¶
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.
type Endpoint ¶
Endpoint represents the spec of an API endpoint, a function that creates http request which fulfills all requirements the endpoint need.
func (Endpoint) DefaultCaller ¶
DefaultCaller is shortcut to ep.SendBy(nil).ParseWith(DefaultParser)
type Parser ¶
Parser parses server response and decode it. It takes responsibility to close response body.
type TypedCaller ¶
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.