roamer

package module
v1.7.2 Latest Latest
Warning

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

Go to latest
Published: May 2, 2024 License: MIT Imports: 10 Imported by: 0

README

Go Report Card Build Status Coverage Status Go Reference GitHub release

roamer

Flexible http request parser

Install

go get -u github.com/slipros/roamer@latest

Decoder

Decode body of http request based on Content-Type header.

Type Content-Type
json application/json
xml application/xml
form application/x-www-form-urlencoded
multipart multipart/form-data
custom any
Json decoder with custom content type
package main

import (
	"github.com/slipros/roamer"
	"github.com/slipros/roamer/decoder"
)

func main() {
	_ = roamer.NewRoamer(
		roamer.WithDecoders(
			decoder.NewJSON(decoder.WithContentType[*decoder.JSON]("my content type")),
		),
	)
}

Parser

Parsing data from source.

Type Source
header http header
query http query
path router path
custom any

Examples

curl --location 'http://127.0.0.1:3000?int=1&int8=2&int16=3&int32=4&int64=5&time=2021-01-01T02%3A07%3A14Z&custom_type=value' \
--header 'Content-Type: application/json' \
--data-raw '{
    "string": "Hello",
    "email": "test@test.com"
}'
package main

import (
	"encoding/json"
	"net/http"
	"time"

	"github.com/slipros/roamer"
	"github.com/slipros/roamer/decoder"
	"github.com/slipros/roamer/parser"
	"github.com/go-chi/chi/v5"
	"github.com/go-chi/chi/v5/middleware"
)

type Custom string

const (
	CustomValue Custom = "value"
)

type Body struct {
	String string  `json:"string"`
	Email  *string `json:"email"`

	Int        int       `query:"int"`
	Int8       int8      `query:"int8"`
	Int16      int16     `query:"int16"`
	Int32      int32     `query:"int32"`
	Int64      int64     `query:"int64"`
	Time       time.Time `query:"time"`
	CustomType *Custom   `query:"custom_type"`
}

func main() {
	r := roamer.NewRoamer(
		roamer.WithDecoders(decoder.NewJSON()),
		roamer.WithParsers(parser.NewQuery()),
	)

	router := chi.NewRouter()
	router.Use(middleware.Logger, roamer.Middleware[Body](r))
	router.Post("/", func(w http.ResponseWriter, r *http.Request) {
		var body Body
		if err := roamer.ParsedDataFromContext(r.Context(), &body); err != nil {
			w.Write([]byte(err.Error()))
			return
		}

		if err := json.NewEncoder(w).Encode(&body); err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return
		}
	})
	
	if err := http.ListenAndServe(":3000", router); err != nil {
		panic(err)
	}
}
With path parser
curl --location --request POST 'http://127.0.0.1:3000/test/some_value?int=1' \
--header 'User-Agent: PostmanRuntime/7.33.0'
package main

import (
	"encoding/json"
	"net/http"

	"github.com/slipros/roamer"
	"github.com/slipros/roamer/parser"
	rchi "github.com/slipros/roamer/pkg/chi"
	"github.com/go-chi/chi/v5"
	"github.com/go-chi/chi/v5/middleware"
)

type Body struct {
	Path      string `path:"path"` // after parse value will be = some_value
	UserAgent string `header:"User-Agent"` // after parse value will be = PostmanRuntime/7.33.0
	Int       int    `query:"int"` // after parse value will be = 1
}

func main() {
	router := chi.NewRouter()

	r := roamer.NewRoamer(
		roamer.WithParsers(
			parser.NewHeader(),                        // parse http headers
			parser.NewQuery(),                         // parse http query params
			parser.NewPath(rchi.NewPath(router)), // parse http path params
		),
	)

	router.Use(middleware.Logger, roamer.Middleware[Body](r))
	router.Post("/test/{path}", func(w http.ResponseWriter, r *http.Request) {
		var body Body
		if err := roamer.ParsedDataFromContext(r.Context(), &body); err != nil {
			w.Write([]byte(err.Error()))
			return
		}

		if err := json.NewEncoder(w).Encode(&body); err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return
		}
	})
	
	if err := http.ListenAndServe(":3000", router); err != nil {
		panic(err)
	}
}
With custom parser
package main

import (
	"context"
	"encoding/json"
	"net/http"
	"reflect"

	"github.com/slipros/roamer"
	"github.com/slipros/roamer/parser"
	"github.com/go-chi/chi/v5"
	"github.com/go-chi/chi/v5/middleware"
	"github.com/gofrs/uuid"
)

type ContextKey string

const (
	ContextKeyProfile ContextKey = "profile"
)

type Profile struct {
	Age      int
	Email    string
	ClientID uuid.UUID
}

const (
	TagProfile = "profile"
)

type ProfileParser struct{}

func (p *ProfileParser) Parse(r *http.Request, tag reflect.StructTag, _ parser.Cache) (any, bool) {
	tagValue, ok := tag.Lookup(TagProfile)
	if !ok {
		return nil, false
	}

	profile, ok := r.Context().Value(ContextKeyProfile).(*Profile)
	if !ok {
		return nil, false
	}

	var v any
	switch tagValue {
	case "client_id":
		v = profile.ClientID
	case "email":
		v = profile.Email
	case "age":
		v = &profile.Age
	case "profile":
		v = profile
	default:
		return nil, false
	}

	return v, true
}

func (p *ProfileParser) Tag() string {
	return TagProfile
}

type Body struct {
	ClientID   *uuid.UUID `profile:"client_id"`
	Age        int        `profile:"age"`
	ProfilePtr *Profile   `profile:"profile"`
	Profile    Profile    `profile:"profile"`
}

func main() {
	r := roamer.NewRoamer(
		roamer.WithParsers(
			&ProfileParser{}, // parse profile from context
		),
	)

	router := chi.NewRouter()

	profileMiddleware := func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			profile := Profile{
				Email:    "profile@profile.com",
				ClientID: uuid.FromStringOrNil("e4aa78cd-a98a-4d9e-84ee-fea61c1c047b"),
				Age:      100,
			}

			ctxWithProfile := context.WithValue(r.Context(), ContextKeyProfile, &profile)
			next.ServeHTTP(w, r.WithContext(ctxWithProfile))
		})
	}

	router.Use(middleware.Logger, profileMiddleware, roamer.Middleware[Body](r))
	router.Post("/", func(w http.ResponseWriter, r *http.Request) {
		var body Body
		if err := roamer.ParsedDataFromContext(r.Context(), &body); err != nil {
			w.Write([]byte(err.Error()))
			return
		}

		if err := json.NewEncoder(w).Encode(&body); err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return
		}
	})
	
	if err := http.ListenAndServe(":3000", router); err != nil {
		panic(err)
	}
}
With multipart/form-data decoder
curl --location 'http://127.0.0.1:3000' \
--header 'X-Referer: http://localhost:3000' \
--header 'Authorization: Bearer 018ad70c-6c98-789c-ac0e-b8e51931e628' \
--form 'campaignId="campaign"' \
--form 'fileId="1337"' \
--form 'file=@"/C:/Users/slipros/Downloads/devices.csv"' \
--form 'file2=@"/C:/Users/slipros/Downloads/devices.csv"'
package main

import (
	"encoding/json"
	"net/http"

	"github.com/slipros/roamer"
	"github.com/slipros/roamer/decoder"
	"github.com/go-chi/chi/v5"
	"github.com/go-chi/chi/v5/middleware"
)

type UploadDevicesFile struct {
	CampaignID string                 `multipart:"campaignId"` // after parse multipart/form-data key campaignId = campaign
	FileID     int                    `multipart:"fileId"`     // parse multipart/form-data key fileId = 1337
	File       *decoder.MultipartFile `multipart:"file"`       // parse multipart/form-data key file
	Files      decoder.MultipartFiles `multipart:",allfiles"`  // parse all multipart/form-data files = [file, file2]
}

func main() {
	r := roamer.NewRoamer(
		roamer.WithDecoders(
			decoder.NewMultipartFormData(),
		),
	)

	router := chi.NewRouter()
	router.Use(middleware.Logger, roamer.Middleware[UploadDevicesFile](r))
	router.Post("/", func(w http.ResponseWriter, r *http.Request) {
		var body UploadDevicesFile
		if err := roamer.ParsedDataFromContext(r.Context(), &body); err != nil {
			w.Write([]byte(err.Error()))
			return
		}

		if err := json.NewEncoder(w).Encode(&body); err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return
		}
	})
	
	if err := http.ListenAndServe(":3000", router); err != nil {
		panic(err)
    }
}

Experimental

FastStructFieldParser

Reduces the number of heap memory allocations.

package main

import (
	"github.com/slipros/roamer"
	"github.com/slipros/roamer/parser"
)

func main() {
	r := roamer.NewRoamer(
		roamer.WithParsers(parser.NewHeader(), parser.NewQuery()),
		roamer.WithExperimentalFastStructFieldParser(), // enables experimental fast struct field parser
	)
}
goos: windows
goarch: amd64
pkg: github.com/slipros/roamer
cpu: 12th Gen Intel(R) Core(TM) i9-12900K
BenchmarkParse_With_Body_Header_Query
BenchmarkParse_With_Body_Header_Query-16                                  
469662              2528 ns/op            1288 B/op         32 allocs/op
BenchmarkParse_With_Body_Header_Query_FastStructFieldParser
BenchmarkParse_With_Body_Header_Query_FastStructFieldParser-16            
498758              2508 ns/op            1224 B/op         24 allocs/op

BenchmarkFormURL_Decode
BenchmarkFormURL_Decode-16                                
516001              2381 ns/op             480 B/op         40 allocs/op
BenchmarkFormURL_Decode_FastStructFieldParser
BenchmarkFormURL_Decode_FastStructFieldParser-16          
524096              2247 ns/op             320 B/op         20 allocs/op

BenchmarkMultipartFormData_Decode
BenchmarkMultipartFormData_Decode-16                              
811407              1560 ns/op             984 B/op         32 allocs/op
BenchmarkMultipartFormData_Decode_FastStructFieldParser
BenchmarkMultipartFormData_Decode_FastStructFieldParser-16        
887547              1496 ns/op             904 B/op         22 allocs/op
PASS

Documentation

Overview

Package roamer provides flexible http request parser.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func ContextWithParsedData

func ContextWithParsedData(ctx context.Context, data any) context.Context

ContextWithParsedData returns a context with parsed data.

func IsDecodeError

func IsDecodeError(err error) (rerr.DecodeError, bool)

IsDecodeError checks the error for belonging to decode error.

func IsSliceIterationError

func IsSliceIterationError(err error) (rerr.SliceIterationError, bool)

IsSliceIterationError checks the error for belonging to slice iteration error.

func Middleware

func Middleware[T any](roamer *Roamer) func(next http.Handler) http.Handler

Middleware parse http request and saves the received value/error to context.

func ParsedDataFromContext

func ParsedDataFromContext[T any](ctx context.Context, ptr *T) error

ParsedDataFromContext return parsed data from context.

Types

type AfterParser

type AfterParser interface {
	AfterParse(r *http.Request) error
}

AfterParser will be called after http request parsing.

type ContextKey

type ContextKey uint8

ContextKey context key.

const (
	// ContextKeyParsedData is a key for parsed data.
	ContextKeyParsedData ContextKey = iota + 1
	// ContextKeyParsingError is a key for parsing error.
	ContextKeyParsingError
)

type Decoder

type Decoder interface {
	Decode(r *http.Request, ptr any) error
	ContentType() string
}

Decoder is a decoder.

type Decoders

type Decoders map[string]Decoder

Decoders is a map of decoders where keys are content types for given decoders.

type OptionsFunc

type OptionsFunc func(*Roamer)

OptionsFunc function for setting options.

func WithDecoders

func WithDecoders(decoders ...Decoder) OptionsFunc

WithDecoders sets decoders.

func WithExperimentalFastStructFieldParser added in v1.7.0

func WithExperimentalFastStructFieldParser() OptionsFunc

WithExperimentalFastStructFieldParser enables the use of experimental fast struct field parser.

func WithParsers

func WithParsers(parsers ...Parser) OptionsFunc

WithParsers sets parsers.

func WithSkipFilled

func WithSkipFilled(skip bool) OptionsFunc

WithSkipFilled sets skip filled.

type Parser

type Parser interface {
	Parse(r *http.Request, tag reflect.StructTag, cache parser.Cache) (any, bool)
	Tag() string
}

Parser is a parser.

type Parsers

type Parsers map[string]Parser

Parsers is a map of parsers where keys are tags for given parsers.

type Roamer

type Roamer struct {
	// contains filtered or unexported fields
}

Roamer flexible http request parser.

func NewRoamer

func NewRoamer(opts ...OptionsFunc) *Roamer

NewRoamer creates and returns new roamer.

func (*Roamer) Parse

func (r *Roamer) Parse(req *http.Request, ptr any) error

Parse parses http request into ptr.

ptr can implement AfterParser to execute some logic after parsing.

Directories

Path Synopsis
Package decoder provides decoders.
Package decoder provides decoders.
Package err contains roamer errors.
Package err contains roamer errors.
internal
experiment
Package experiment contains experimental features.
Package experiment contains experimental features.
Package parser provides parsers.
Package parser provides parsers.
pkg
chi Module
gorilla Module
Package value converts one value type to another.
Package value converts one value type to another.

Jump to

Keyboard shortcuts

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