Flexible http request parser
go get -u github.com/slipros/roamer@latest
Format parsed data.
Type | Available values |
---|---|
string | trim_space |
custom |
any |
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 |
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")),
),
)
}
Parsing data from source.
Type | Source |
---|---|
header | http header |
cookie | http cookie |
query | http query |
path | router path |
custom |
any |
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": "[email protected]"
}'
package main
import (
"encoding/json"
"net/http"
"time"
"github.com/slipros/roamer"
"github.com/slipros/roamer/decoder"
"github.com/slipros/roamer/formatter"
"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" string:"trim_space"`
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()),
roamer.WithFormatters(formatter.NewString()),
)
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)
}
}
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
Referer string `header:"Referer,X-Referer"` // try to parse value from any of available headers
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)
}
}
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: "[email protected]",
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)
}
}
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)
}
}
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