fetch

package module
v1.0.2 Latest Latest
Warning

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

Go to latest
Published: Mar 13, 2025 License: BSD-3-Clause, MIT Imports: 13 Imported by: 3

README

FETCH
Go HTTP client. Inspired by the simplicity of axios and JSON handling in JS. Improved with generics.

Reasons

  1. I was tired of writing json:"fieldName,omitempty" alongside each field. This package de-capitalizes the public fields in JSON parsing and omits empty ones unless json tag is specified.
  2. I always forget all the boilerplate code to make an HTTP request. This package provides a simple one-function call approach.

Installing

This is a zero-dependency package. It requires Go version 1.21 or above. Stable version is out!

go get github.com/glossd/fetch

Structure

Functions of the fetch package match HTTP methods. Each function is generic and its generic type is the response.

$${\color{lightblue}fetch. \color{lightgreen}Method \color{lightblue}[ \color{cyan}ResponseType \color{lightblue}](url \space string, \space ...) \space returns \space \color{cyan}ResponseType}$$

Examples

This is the Pet object from https://petstore.swagger.io/

{
  "id": 1,
  "name": "Buster",
  "tags": [
    {
      "name": "beagle"
    }
  ]
}

GET request

Print response
str, err := fetch.Get[string]("https://petstore.swagger.io/v2/pet/1")
if err != nil {
    //handle err
}
fmt.Println(str)
Dynamically typed

fetch.J is an interface representing arbitrary JSON.

j, err := fetch.Get[fetch.J]("https://petstore.swagger.io/v2/pet/1")
if err != nil {
    panic(err)
}
// access nested values using jq-like patterns
fmt.Println("Pet's name is ", j.Q(".name"))
fmt.Println("First tag's name is ", j.Q(".tags[0].name"))

More about jq-like patterns

Statically typed
type Tag struct {
    Name string
}
type Pet struct {
    Name string
    Tags []Tag
}

pet, err := fetch.Get[Pet]("https://petstore.swagger.io/v2/pet/1")
if err != nil {
    panic(err)
}
fmt.Println("Pet's name is ", pet.Name)
fmt.Println("First tag's name is ", pet.Tags[0].Name) // handle index

POST request

Post, Put and others have an additional argument for the request body of any type.

type Pet struct {
    Name string
}
type IdObj struct {
    Id int
}
obj, err := fetch.Post[IdObj]("https://petstore.swagger.io/v2/pet", Pet{Name: "Lola"})
if err != nil {
    panic(err)
}
fmt.Println("Posted pet's ID ", obj.Id)

*Passing string or []byte type variable as the second argument will directly add its value to the request body.

HTTP response status, headers and other attributes

If you need to check the status or headers of the response, you can wrap your response type with fetch.Response.

type Pet struct {
    Name string
}
resp, err := fetch.Get[fetch.Response[Pet]]("https://petstore.swagger.io/v2/pet/1")
if err != nil {
    panic(err)
}
if resp.Status == 200 {
    fmt.Println("Found pet with id 1")
    // Response.Body is the Pet object.
    fmt.Println("Pet's name is ", resp.Body.Name)
    fmt.Println("Response headers", resp.Headers)
}

If you don't need the HTTP body you can use fetch.Empty or fetch.Response[fetch.Empty] to access http attributes

res, err := fetch.Delete[fetch.Response[fetch.Empty]]("https://petstore.swagger.io/v2/pet/10")
if err != nil {
    panic(err)
}
fmt.Println("Status:", res.Status)
fmt.Println("Headers:", res.Headers)
Error handling

Any non-2xx response status is treated as an error! If the error isn't nil it can be safely cast to *fetch.Error which will contain the status and other HTTP attributes.

_, err := fetch.Get[string]("https://petstore.swagger.io/v2/pet/-1")
if err != nil {
    fmt.Printf("Get pet failed: %s\n", err)
    ferr := err.(*fetch.Error)
    fmt.Printf("HTTP status=%d, headers=%v, body=%s", ferr.Status, ferr.Headers, ferr.Body)
}

Make request with Go Context

Request Context lives in fetch.Config

func myFuncWithContext(ctx context.Context) {
    ...
    res, err := fetch.Get[string]("https://petstore.swagger.io/v2/pet/1", fetch.Config{Ctx: ctx})
    ...
}

Request with 5 seconds timeout:

fetch.Get[string]("https://petstore.swagger.io/v2/pet/1", fetch.Config{Timeout: 5*time.Second})

Request with headers

headers := map[string]string{"Content-type": "text/plain"}
_, err := fetch.Get[string]("https://petstore.swagger.io/v2/pet/1", fetch.Config{Headers: headers})
if err != nil {
    panic(err)
}

Capitalized fields

If you want this package to parse the public fields as capitalized into JSON, you need to add the json tag:

type Pet struct {
    Name string `json:"Name"`
}

Arrays

Simple, just pass an array as the response type.

type Pet struct {
    Name string
}
pets, err := fetch.Get[[]Pet]("https://petstore.swagger.io/v2/pet/findByStatus?status=sold")
if err != nil {
    panic(err)
}
fmt.Println("First sold pet ", pets[0]) // handle index out of range

Or you can use fetch.J

j, err := fetch.Get[fetch.J]("https://petstore.swagger.io/v2/pet/findByStatus?status=sold")
if err != nil {
    panic(err)
}
fmt.Println("First sold pet ", j.Q(".[0]"))

JQ-like queries

fetch.J is an interface with Q method which provides easy access to any field.
Method fetch.J#String() returns JSON formatted string of the value.

j := fetch.Parse(`{
    "name": "Jason",
    "category": {
        "name":"dogs"
    },
    "tags": [
        {"name":"briard"}
    ]
}`)

fmt.Println("Print the whole json:", j)
fmt.Println("Pet's name is", j.Q(".name"))
fmt.Println("Pet's category name is", j.Q(".category.name"))
fmt.Println("First tag's name is", j.Q(".tags[0].name"))

Method fetch.J#Q returns fetch.J. You can use the method Q on the result as well.

category := j.Q(".category")
fmt.Println("Pet's category object", category)
fmt.Println("Pet's category name is", category.Q(".name"))

To convert fetch.J to a basic value use one of As* methods

J Method Return type
AsObject map[string]any
AsArray []any
AsNumber float64
AsString string
AsBoolean bool

E.g.

n, ok := fetch.Parse(`{"price": 14.99}`).Q(".price").AsNumber()
if !ok {
    // not a number
}
fmt.Printf("Price: %.2f\n", n) // n is a float64

Use IsNil to check the value on presence.

if fetch.Parse("{}").Q(".price").IsNil() {
    fmt.Println("key 'price' doesn't exist")
}
// fields of unknown values are nil as well.
if fetch.Parse("{}").Q(".price.cents").IsNil() {
    fmt.Println("'cents' of undefined is fine.")
}

JSON handling

I have patched encoding/json package and attached to the internal folder, but you can use these functions.

Marhsal

Use it to convert any object into a string. It's the same as json.Marshal but it treats public struct fields as de-capitalized and omits empty fields by default unless json tag is specified.

str, err := fetch.Marhsal(map[string]string{"key":"value"})

Unmarshalling

Unmarshal will parse the input into the generic type.

type Pet struct {
    Name string
}
p, err := fetch.Unmarshal[Pet](`{"name":"Jason"}`)
if err != nil {
    panic(err)
}
fmt.Println(p.Name)

fetch.Parse unmarshalls JSON string into fetch.J, returning fetch.Nil instead of an error, which allows you to write one-liners.

fmt.Println(fetch.Parse(`{"name":"Jason"}`).Q(".name"))

Global Setters

You can set base URL path for all requests.

fetch.SetBaseURL("https://petstore.swagger.io/v2")
pet := fetch.Get[string]("/pets/1")
// you can still call other URLs by passing URL with protocol.
fetch.Get[string]("https://www.google.com")

You can set the http.Client for all requests

fetch.SetHttpClient(&http.Client{Timeout: time.Minute})

fetch.Config

Each HTTP method has the configuration option.

type Config struct {
    // Defaults to context.Background()
    Ctx context.Context
    // Sets Ctx with the specified timeout. If Ctx is specified Timeout is ignored.
    Timeout time.Duration
    // Defaults to GET
    Method  string
    Body    string
    Headers map[string]string
}

HTTP Handlers

fetch.ToHandlerFunc converts func(in) (out, error) signature function into http.HandlerFunc. It does all the json and http handling for you. The HTTP request body unmarshalls into the function argument. The return value is marshaled into the HTTP response body.

type Pet struct {
    Name string
}
http.HandleFunc("/pets/update", fetch.ToHandlerFunc(func(in Pet) (*Pet, error) {
    if in.Name == "" {
        return nil, fmt.Errorf("name can't be empty")
    }
    return &Pet{Name: in.Name + " 3000"}, nil
}))
http.ListenAndServe(":8080", nil)
$ curl localhost:8080/pets/update -d '{"name":"Lola"}'
{"name":"Lola 3000"}
Ignoring request or response

If you have an empty request or response body or you want to ignore them, use fetch.Empty:

http.HandleFunc("/default-pet", fetch.ToHandlerFunc(func(_ fetch.Empty) (Pet, error) {
    return Pet{Name: "Teddy"}, nil
}))

Alternatively, you can use fetch.ToHandlerFuncEmptyIn and fetch.ToHandlerFuncEmptyOut functions.

Wrappers

If you need to access http request attributes wrap the input with fetch.Request:

type Pet struct {
    Name string
}
http.HandleFunc("/pets", fetch.ToHandlerFunc(func(req fetch.Request[Pet]) (*fetch.Empty, error) {
    fmt.Println("Request context:", req.Context)
    fmt.Println("Authorization header:", req.Headers["Authorization"])
    fmt.Println("Pet:", req.Body)
    fmt.Println("Pet's name:", req.Body.Name)
    return nil, nil
}))

If you have go1.23 and above you can access the wildcards as well.

http.HandleFunc("GET /pets/{id}", fetch.ToHandlerFunc(func(in fetch.Request[fetch.Empty]) (*fetch.Empty, error) {
    fmt.Println("id from url:", in.PathValues["id"])
    return nil, nil
}))

To customize http attributes of the response, wrap the output with fetch.Response

http.HandleFunc("/pets", fetch.ToHandlerFunc(func(_ fetch.Empty) (fetch.Response[*Pet], error) {
    return Response[*Pet]{Status: 201, Body: &Pet{Name: "Lola"}}, nil
}))

The error format can be customized with the fetch.SetHandlerErrorFormat global setter.
To log ToHandleFunc errors with your logger call SetHandlerConfig

fetch.SetHandlerConfig(fetch.HandlerConfig{ErrorHook: func(err error) {
    mylogger.Errorf("fetch http error: %s", err)
}})

To add middleware before handling request in fetch.ToHandlerFunc

fetch.SetHandlerConfig(fetch.HandlerConfig{Middleware: func(w http.ResponseWriter, r *http.Request) bool {
    if r.Header.Get("Authorization") == "" {
        w.WriteHeader(401)
        return true
    }
    return false
}})

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Delete

func Delete[T any](url string, config ...Config) (T, error)

func Do added in v0.6.0

func Do[T any](url string, config ...Config) (T, error)

func Get

func Get[T any](url string, config ...Config) (T, error)
func Head[T any](url string, config ...Config) (T, error)

func Marshal

func Marshal(v any) (string, error)

Marshal calls the patched json.Marshal function. There are only two patches

  1. It lowercases the first letter of the public struct fields.
  2. It omits empty fields by default.

They are only applied if the `json` tag is not specified.

func Options

func Options[T any](url string, config ...Config) (T, error)

func Patch

func Patch[T any](url string, body any, config ...Config) (T, error)

func Post

func Post[T any](url string, body any, config ...Config) (T, error)

func Put

func Put[T any](url string, body any, config ...Config) (T, error)

func SetBaseURL

func SetBaseURL(b string)

func SetHandlerConfig added in v1.0.0

func SetHandlerConfig(hc HandlerConfig)

SetHandlerConfig sets HandlerConfig globally to be applied for every ToHandlerFunc.

func SetHandlerErrorFormat added in v1.0.0

func SetHandlerErrorFormat(format string)

SetHandlerErrorFormat is a global setter configuring how ToHandlerFunc converts errors returned from ApplyFunc. format argument must contain only one %s verb which would be the error message. Defaults to {"error":"%s"} Examples: fetch.SetHandlerErrorFormat(`{"msg":"%s"}`) fetch.SetHandlerErrorFormat("%s") - just plain error text fetch.SetHandlerErrorFormat(`{"error":{"message":"%s"}}`)

func SetHttpClient

func SetHttpClient(c *http.Client)

func ToHandlerFunc added in v0.4.4

func ToHandlerFunc[In any, Out any](apply ApplyFunc[In, Out]) http.HandlerFunc

ToHandlerFunc converts ApplyFunc into http.HandlerFunc, which can be used later in http.ServeMux#HandleFunc. It unmarshals the HTTP request body into the ApplyFunc argument and then marshals the returned value into the HTTP response body. To access HTTP request attributes, wrap your input in fetch.Request.

func ToHandlerFuncEmptyIn added in v1.0.1

func ToHandlerFuncEmptyIn[Out any](supply SupplyFunc[Out]) http.HandlerFunc

func ToHandlerFuncEmptyOut added in v1.0.1

func ToHandlerFuncEmptyOut[In any](consume ConsumeFunc[In]) http.HandlerFunc

func Unmarshal

func Unmarshal[T any](j string) (T, error)

Unmarshal is a generic wrapper for UnmarshalInto

func UnmarshalInto

func UnmarshalInto(j string, v any) error

UnmarshalInto calls the patched json.Unmarshal function. The only difference between them is it handles `fetch.J` and transforms `any` into fetch.J.

func UnmarshalJ added in v0.2.2

func UnmarshalJ[T any](j J) (T, error)

UnmarshalJ unmarshalls J into the generic value.

Types

type A

type A []any

A represents a JSON array

func (A) AsArray added in v0.2.2

func (a A) AsArray() ([]any, bool)

func (A) AsBoolean added in v0.2.2

func (a A) AsBoolean() (bool, bool)

func (A) AsNumber added in v0.2.2

func (a A) AsNumber() (float64, bool)

func (A) AsObject added in v0.2.2

func (a A) AsObject() (map[string]any, bool)

func (A) AsString added in v0.2.2

func (a A) AsString() (string, bool)

func (A) Elem added in v1.0.0

func (a A) Elem() any

func (A) IsNil added in v0.2.2

func (a A) IsNil() bool

func (A) Q

func (a A) Q(pattern string) J

func (A) String

func (a A) String() string

type ApplyFunc added in v0.4.4

type ApplyFunc[In any, Out any] func(in In) (Out, error)

ApplyFunc represents a simple function to be converted to http.Handler with In type as a request body and Out type as a response body.

type B added in v0.2.1

type B bool

B represents a JSON boolean

func (B) AsArray added in v0.2.2

func (b B) AsArray() ([]any, bool)

func (B) AsBoolean added in v0.2.2

func (b B) AsBoolean() (bool, bool)

func (B) AsNumber added in v0.2.2

func (b B) AsNumber() (float64, bool)

func (B) AsObject added in v0.2.2

func (b B) AsObject() (map[string]any, bool)

func (B) AsString added in v0.2.2

func (b B) AsString() (string, bool)

func (B) Elem added in v1.0.0

func (b B) Elem() any

func (B) IsNil added in v0.2.2

func (b B) IsNil() bool

func (B) Q added in v0.2.1

func (b B) Q(pattern string) J

func (B) String added in v0.2.1

func (b B) String() string

type Config

type Config struct {
	// Defaults to context.Background()
	Ctx context.Context
	// Sets Ctx with the specified timeout. If Ctx is specified Timeout is ignored.
	Timeout time.Duration
	// Defaults to GET
	Method  string
	Body    string
	Headers map[string]string
}

type ConsumeFunc added in v1.0.1

type ConsumeFunc[In any] func(in In) error

ConsumeFunc serves as ApplyFunc ignoring HTTP response

type Empty added in v0.4.4

type Empty struct{}

Empty represents an empty response or request body, skipping JSON handling. Can be used with the wrappers Response and Request or to fit the signature of ApplyFunc.

type Error

type Error struct {
	Msg     string
	Status  int
	Headers map[string]string
	Body    string
	// contains filtered or unexported fields
}

func (*Error) Error

func (e *Error) Error() string

func (*Error) Unwrap

func (e *Error) Unwrap() error

type F

type F float64

F represents JSON number.

func (F) AsArray added in v0.2.2

func (f F) AsArray() ([]any, bool)

func (F) AsBoolean added in v0.2.2

func (f F) AsBoolean() (bool, bool)

func (F) AsNumber added in v0.2.2

func (f F) AsNumber() (float64, bool)

func (F) AsObject added in v0.2.2

func (f F) AsObject() (map[string]any, bool)

func (F) AsString added in v0.2.2

func (f F) AsString() (string, bool)

func (F) Elem added in v1.0.0

func (f F) Elem() any

func (F) IsNil added in v0.2.2

func (f F) IsNil() bool

func (F) Q

func (f F) Q(pattern string) J

func (F) String

func (f F) String() string

type HandlerConfig added in v0.4.4

type HandlerConfig struct {
	// ErrorHook is called if an error happens while sending an HTTP response
	ErrorHook func(err error)
	// Middleware is applied before ToHandlerFunc processes the request.
	// Return true to end the request processing.
	Middleware func(w http.ResponseWriter, r *http.Request) bool
}

type J

type J interface {
	/*
		Q parses jq-like patterns and returns according to the path value.
		If the value wasn't found or syntax is incorrect, Q will return Nil.
		Examples:

		j := fetch.Parse(`{
		  "name": "Jason",
		  "category": {
		    "name":"dogs"
		  }
		  "tags": [{"name":"briard"}]
		}`)

		Print json: fmt.Println(j)
		Retrieve name: j.Q(".name")
		Retrieve category name: j.Q(".category.name")
		Retrieve first tag's name: j.Q(".tags[0].name")
	*/
	Q(pattern string) J

	// String returns JSON formatted string.
	String() string

	// Elem converts J to its definition and returns it.
	// Type-Definitions:
	// M -> map[string]any
	// A -> []any
	// F -> float64
	// S -> string
	// B -> bool
	// Nil -> nil
	Elem() any

	// AsObject is a convenient type assertion if J is a map[string]any.
	AsObject() (map[string]any, bool)
	// AsArray is a convenient type assertion if J is a slice of type []any.
	AsArray() ([]any, bool)
	// AsNumber is a convenient type assertion if J is a float64.
	AsNumber() (float64, bool)
	// AsString is a convenient type assertion if J is a string.
	AsString() (string, bool)
	// AsBoolean is a convenient type assertion if J is a bool.
	AsBoolean() (bool, bool)
	// IsNil check if J is fetch.Nil
	IsNil() bool
}

J represents arbitrary JSON. Depending on the JSON data type the queried `fetch.J` could be one of these types

| Type | Go definition | JSON data type | |-----------|-----------------|-------------------------------------| | fetch.M | map[string]any | object | | fetch.A | []any | array | | fetch.F | float64 | number | | fetch.S | string | string | | fetch.B | bool | boolean | | fetch.Nil | (nil) *struct{} | null, undefined, anything not found |

func Parse added in v0.2.5

func Parse(s string) J

Parse unmarshalls the JSON string into fetch.J without panicking. If unmarshalling encounters an error, Parse returns fetch.Nil type.

type M

type M map[string]any

M represents a JSON object.

func (M) AsArray added in v0.2.2

func (m M) AsArray() ([]any, bool)

func (M) AsBoolean added in v0.2.2

func (m M) AsBoolean() (bool, bool)

func (M) AsNumber added in v0.2.2

func (m M) AsNumber() (float64, bool)

func (M) AsObject added in v0.2.2

func (m M) AsObject() (map[string]any, bool)

func (M) AsString added in v0.2.2

func (m M) AsString() (string, bool)

func (M) Elem added in v1.0.0

func (m M) Elem() any

func (M) IsNil added in v0.2.2

func (m M) IsNil() bool

func (M) Q

func (m M) Q(pattern string) J

func (M) String

func (m M) String() string

type Nil added in v0.2.0

type Nil = *nilStruct

Nil represents any not found or null values. It is also used for syntax errors. Nil is pointer which value is always nil. However, when returned from any method, it doesn't equal nil, because a Go interface is not nil when it has a type. It exists to prevent nil pointer dereference when retrieving Elem value. It can be the root of J tree, because null alone is a valid JSON.

func (Nil) AsArray added in v0.2.2

func (n Nil) AsArray() ([]any, bool)

func (Nil) AsBoolean added in v0.2.2

func (n Nil) AsBoolean() (bool, bool)

func (Nil) AsNumber added in v0.2.2

func (n Nil) AsNumber() (float64, bool)

func (Nil) AsObject added in v0.2.2

func (n Nil) AsObject() (map[string]any, bool)

func (Nil) AsString added in v0.2.2

func (n Nil) AsString() (string, bool)

func (Nil) Elem added in v1.0.0

func (n Nil) Elem() any

func (Nil) IsNil added in v0.2.2

func (n Nil) IsNil() bool

func (Nil) Q added in v0.2.0

func (n Nil) Q(string) J

func (Nil) String added in v0.2.0

func (n Nil) String() string

type Request

type Request[T any] struct {
	Context context.Context
	// Only available in go1.23 and above.
	// PathValue was introduced in go1.22 but
	// there was no reliable way to extract them.
	// go1.23 introduced http.Request.Pattern allowing to list the wildcards.
	PathValues map[string]string
	// URL parameters.
	Parameters map[string]string
	// HTTP headers.
	Headers map[string]string
	Body    T
}

Request can be used in ApplyFunc as a wrapper for the input entity to access http attributes. e.g.

type Pet struct {
	Name string
}
http.HandleFunc("POST /pets/{id}", fetch.ToHandlerFunc(func(in fetch.Request[Pet]) (fetch.Empty, error) {
	in.Context()
	return fetch.Empty{}, nil
}))

func (Request[T]) WithHeader added in v1.0.0

func (r Request[T]) WithHeader(name, value string) Request[T]

func (Request[T]) WithParameter added in v1.0.0

func (r Request[T]) WithParameter(name, value string) Request[T]

func (Request[T]) WithPathValue added in v1.0.0

func (r Request[T]) WithPathValue(name, value string) Request[T]

type RequestEmpty added in v1.0.2

type RequestEmpty = Request[Empty]

type Response

type Response[T any] struct {
	Status  int
	Headers map[string]string
	Body    T
}

Response is a wrapper type for (generic) ReturnType to be used in the HTTP methods. It allows you to access HTTP attributes of the HTTP response and unmarshal the HTTP body. e.g.

type User struct {
	FirstName string
}
res, err := Get[Response[User]]("/users/1")
if err != nil {panic(err)}
if res.Status != 202 {
   panic("unexpected status")
}
// Body is User type
fmt.Println(res.Body.FirstName)

type ResponseEmpty added in v0.2.6

type ResponseEmpty = Response[Empty]

type S

type S string

S represents JSON string.

func (S) AsArray added in v0.2.2

func (s S) AsArray() ([]any, bool)

func (S) AsBoolean added in v0.2.2

func (s S) AsBoolean() (bool, bool)

func (S) AsNumber added in v0.2.2

func (s S) AsNumber() (float64, bool)

func (S) AsObject added in v0.2.2

func (s S) AsObject() (map[string]any, bool)

func (S) AsString added in v0.2.2

func (s S) AsString() (string, bool)

func (S) Elem added in v1.0.0

func (s S) Elem() any

func (S) IsNil added in v0.2.2

func (s S) IsNil() bool

func (S) Q

func (s S) Q(pattern string) J

func (S) String

func (s S) String() string

type SupplyFunc added in v1.0.1

type SupplyFunc[Out any] func() (Out, error)

SupplyFunc serves as ApplyFunc ignoring HTTP request

Directories

Path Synopsis
internal
json
Package json implements encoding and decoding of JSON as defined in RFC 7159.
Package json implements encoding and decoding of JSON as defined in RFC 7159.

Jump to

Keyboard shortcuts

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