diff --git a/examples/main.go b/examples/main.go index 6383ef4..7ee6acd 100644 --- a/examples/main.go +++ b/examples/main.go @@ -5,24 +5,59 @@ import ( "github.com/grahms/worx/router" ) +type Address struct { + Name *string `json:"name" binding:"required"` +} + +type Spec struct { + Name *string `json:"name"` + Value *string `json:"value"` + Adress *[]*Address `json:"adress"` +} + type Product struct { - Name *string `json:"name" binding:"required"` - Price *float64 `json:"price"` - BaseType *string `json:"@baseType"` - Url *string `json:"@Url"` - ID string `json:"id" binding:"ignore"` + Name *string `json:"name" binding:"required"` + Price *float64 `json:"price"` + BaseType *string `json:"@baseType" binding:"ignore"` + Type *string `json:"type" enums:"physical,digital"` + Url *string `json:"@Url" binding:"ignore"` + IsHumeid *bool `json:"isHumeid" binding:"required"` + Specification *[]Spec `json:"specification"` + Id *string `json:"id" binding:"ignore"` } func main() { - app := worx.NewApplication("/api", "Product Catalog API") + app := worx.NewApplication("/api", "Product Catalog API", "1.0.0", "Product Catalogue API") + productTags := router.WithTags([]string{"Product", "something"}) product := worx.NewRouter[Product, Product](app, "/products") - product.HandleCreate("", - func(product Product, params *router.RequestParams) (*router.ProcessorError, *Product) { - return nil, &product - }) - + product.HandleCreate("", createHandler, router.WithName("product name"), productTags) + product.HandleRead("", handler, productTags) + product.HandleRead("/:id/machava", handler, productTags, router.WithAllowedHeaders([]router.AllowedFields{ + { + Name: "x-tower", + Description: "", + Required: false, + }, + })) err := app.Run(":8080") if err != nil { panic(err) } } + +func createHandler(product Product, params *router.RequestParams) (*router.Err, *Product) { + if *product.Price < 5 { + return &router.Err{ + StatusCode: 409, + ErrCode: "SOME_ERROR", + ErrReason: "Price not goood", + Message: "The message", + }, nil + } + + return nil, &product +} + +func handler(params *router.RequestParams) (*router.Err, *Product) { + return nil, &Product{} +} diff --git a/go.mod b/go.mod index 22709d4..9a1e6a4 100644 --- a/go.mod +++ b/go.mod @@ -7,28 +7,39 @@ require ( github.com/gin-gonic/gin v1.9.1 github.com/grahms/godantic v1.0.11 github.com/stretchr/testify v1.8.4 + github.com/swaggo/files v1.0.1 + github.com/swaggo/gin-swagger v1.6.0 ) require ( + github.com/KyleBanks/depth v1.2.1 // indirect + github.com/PuerkitoBio/purell v1.1.1 // indirect + github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect github.com/bytedance/sonic v1.10.1 // indirect github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect github.com/chenzhuoyu/iasm v0.9.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/gabriel-vasile/mimetype v1.4.2 // indirect github.com/gin-contrib/sse v0.1.0 // indirect + github.com/go-openapi/jsonpointer v0.19.5 // indirect + github.com/go-openapi/jsonreference v0.19.6 // indirect + github.com/go-openapi/spec v0.20.4 // indirect + github.com/go-openapi/swag v0.19.15 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.15.5 // indirect github.com/goccy/go-json v0.10.2 // indirect + github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.2.5 // indirect - github.com/kr/text v0.2.0 // indirect github.com/leodido/go-urn v1.2.4 // indirect + github.com/mailru/easyjson v0.7.6 // indirect github.com/mattn/go-isatty v0.0.19 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pelletier/go-toml/v2 v2.1.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/swaggo/swag v1.8.12 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.11 // indirect golang.org/x/arch v0.5.0 // indirect @@ -36,6 +47,8 @@ require ( golang.org/x/net v0.16.0 // indirect golang.org/x/sys v0.13.0 // indirect golang.org/x/text v0.13.0 // indirect + golang.org/x/tools v0.7.0 // indirect google.golang.org/protobuf v1.31.0 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index b1955c9..0d2af48 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,9 @@ +github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= +github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= +github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI= +github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM= github.com/bytedance/sonic v1.10.1 h1:7a1wuFXL1cMy7a3f7/VFcEtriuXQnUBhtoVfOZiaysc= @@ -16,10 +22,21 @@ github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= github.com/gin-contrib/cors v1.5.0 h1:DgGKV7DDoOn36DFkNtbHrjoRiT5ExCe+PC9/xp7aKvk= github.com/gin-contrib/cors v1.5.0/go.mod h1:TvU7MAZ3EwrPLI2ztzTt3tqgvBCq+wn8WpZmfADjupI= +github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= +github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= +github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonreference v0.19.6 h1:UBIxjkht+AWIgYzCDSv2GN+E/togfwXUJFRTWhl2Jjs= +github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns= +github.com/go-openapi/spec v0.20.4 h1:O8hJrt0UMnhHcluhIdUgCLRWyM2x7QkBXRvOs7m+O1M= +github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM= +github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= @@ -33,22 +50,28 @@ github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaS github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/grahms/godantic v1.0.7 h1:TpUZQRea0NmYPwAFLI/gnFNk7dbAfKoloC31ohwclFM= -github.com/grahms/godantic v1.0.7/go.mod h1:nb4Jbhv0yUk+cFkh/sehBwSxy1EgPST9LgjRBRSrhzE= -github.com/grahms/godantic v1.0.8 h1:Cdb+RgqI3PqglmBaRSl/W6zDEvaNZ+DssON8DKn/gQs= -github.com/grahms/godantic v1.0.8/go.mod h1:nb4Jbhv0yUk+cFkh/sehBwSxy1EgPST9LgjRBRSrhzE= +github.com/grahms/godantic v1.0.11 h1:NF5Ug4eLTDWN9DbcqutTDh+nLAKDrfRSjduXaVcc5ug= github.com/grahms/godantic v1.0.11/go.mod h1:nb4Jbhv0yUk+cFkh/sehBwSxy1EgPST9LgjRBRSrhzE= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg= github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA= +github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -56,6 +79,7 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4= github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -65,6 +89,7 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= @@ -72,31 +97,75 @@ github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE= +github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg= +github.com/swaggo/gin-swagger v1.6.0 h1:y8sxvQ3E20/RCyrXeFfg60r6H0Z+SwpTjMYsMm+zy8M= +github.com/swaggo/gin-swagger v1.6.0/go.mod h1:BG00cCEy294xtVpyIAHG6+e2Qzj/xKlRdOqDkvq0uzo= +github.com/swaggo/swag v1.8.12 h1:pctzkNPu0AlQP2royqX3apjKCQonAnf7KGoxeO4y64w= +github.com/swaggo/swag v1.8.12/go.mod h1:lNfm6Gg+oAq3zRJQNEMBE66LIJKM44mxFqhEEgy2its= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/arch v0.5.0 h1:jpGode6huXQxcskEIpOCvrU+tzo81b6+oFLUYXWtH/Y= golang.org/x/arch v0.5.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.9.0 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.16.0 h1:7eBu7KsSvFDtSXUIDbh3aqlK4DPsZ1rByC8PFfBThos= golang.org/x/net v0.16.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4= +golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= diff --git a/router/endpoint.go b/router/endpoint.go index c9638f0..94c93a2 100644 --- a/router/endpoint.go +++ b/router/endpoint.go @@ -5,17 +5,40 @@ import ( "github.com/gin-gonic/gin" "github.com/grahms/godantic" "io" - "net/http" "strconv" "strings" ) +// Endpoint represents information about an API endpoint +type Endpoint struct { + Path string + Methods []Method +} + +type Method struct { + HTTPMethod string + Request interface{} + Response interface{} + Description string + Tags []string + Summery string + Configs EndpointConfigs +} + +var Endpoints map[string]*Endpoint + +// Initialize the Endpoints map +func init() { + Endpoints = make(map[string]*Endpoint) +} + +// we can have the global struct here type APIEndpointer[Req, Resp any] interface { - HandleCreate(uri string, processRequest func(Req, *RequestParams) (*ProcessorError, *Resp)) - HandleRead(pathSuffix string, processRequest func(*RequestParams) (*ProcessorError, *Resp)) - HandleUpdate(pathString string, requestProcessor func(id string, reqBody Req, params *RequestParams) (*ProcessorError, *Resp)) - HandleList(pathString string, requestProcessor func(params *RequestParams, limit int, offset int) ([]*Resp, *ProcessorError, int, int)) + HandleCreate(uri string, processRequest func(Req, *RequestParams) (*Err, *Resp), opts ...HandleOption) + HandleRead(pathSuffix string, processRequest func(*RequestParams) (*Err, *Resp), opts ...HandleOption) + HandleUpdate(pathString string, requestProcessor func(id string, reqBody Req, params *RequestParams) (*Err, *Resp), opts ...HandleOption) + HandleList(pathString string, requestProcessor func(params *RequestParams, limit int, offset int) ([]*Resp, *Err, int, int), opts ...HandleOption) } func New[In, Out any](path string, group *gin.RouterGroup) *APIEndpoint[In, Out] { @@ -37,9 +60,33 @@ type RequestParams struct { Headers map[string]string PathParams map[string]string } +type EndpointConfigs struct { + Name string + Uri string + Tags []string + Descriptions string + AllowedHeaders []AllowedFields + AllowedParams []AllowedFields +} + +type AllowedFields struct { + Name string + Description string + Required bool +} -func (r *APIEndpoint[Req, Resp]) HandleCreate(uri string, processRequest func(Req, *RequestParams) (*ProcessorError, *Resp)) { +func getConfigs(opts ...HandleOption) *EndpointConfigs { + config := &EndpointConfigs{} + for _, opt := range opts { + opt(config) + } + return config +} +func (r *APIEndpoint[Req, Resp]) HandleCreate(uri string, processRequest func(Req, *RequestParams) (*Err, *Resp), opts ...HandleOption) { + config := getConfigs(opts...) + registerEndpoint(r.Router.BasePath()+r.Path+uri, "POST", new(Req), new(Resp), *config) r.Router.POST(r.Path+uri, func(c *gin.Context) { + if statusCode, exception := r.validateJSONContentType(c); exception != nil { c.JSON(statusCode, exception) return @@ -52,6 +99,7 @@ func (r *APIEndpoint[Req, Resp]) HandleCreate(uri string, processRequest func(Re } params := r.extractRequestParams(c) + perr, response := processRequest(requestBody, ¶ms) if perr != nil { c.JSON(r.validator.ProcessorErr(perr)) @@ -63,7 +111,9 @@ func (r *APIEndpoint[Req, Resp]) HandleCreate(uri string, processRequest func(Re }) } -func (r *APIEndpoint[Req, Resp]) HandleRead(pathSuffix string, processRequest func(*RequestParams) (*ProcessorError, *Resp)) { +func (r *APIEndpoint[Req, Resp]) HandleRead(pathSuffix string, processRequest func(*RequestParams) (*Err, *Resp), opts ...HandleOption) { + config := getConfigs(opts...) + registerEndpoint(r.Router.BasePath()+r.Path+pathSuffix, "GET", new(Req), new(Resp), *config) r.Router.GET(r.Path+pathSuffix, func(c *gin.Context) { reqValues := r.extractRequestParams(c) perr, resp := processRequest(&reqValues) @@ -89,7 +139,9 @@ func (r *APIEndpoint[Req, Resp]) HandleRead(pathSuffix string, processRequest fu }) } -func (r *APIEndpoint[Req, Resp]) HandleUpdate(pathString string, requestProcessor func(id string, reqBody Req, params *RequestParams) (*ProcessorError, *Resp)) { +func (r *APIEndpoint[Req, Resp]) HandleUpdate(pathString string, requestProcessor func(id string, reqBody Req, params *RequestParams) (*Err, *Resp), opts ...HandleOption) { + config := getConfigs(opts...) + registerEndpoint(r.Router.BasePath()+r.Path+pathString, "PATCH", new(Req), new(Resp), *config) binder := godantic.Validate{} binder.IgnoreRequired = true binder.IgnoreMinLen = true @@ -127,12 +179,14 @@ func (r *APIEndpoint[Req, Resp]) convertToMap(obj interface{}) map[string]interf return data } -func (r *APIEndpoint[Req, Resp]) HandleList(pathString string, requestProcessor func(params *RequestParams, limit int, offset int) ([]*Resp, *ProcessorError, int, int)) { +func (r *APIEndpoint[Req, Resp]) HandleList(pathString string, requestProcessor func(params *RequestParams, limit int, offset int) ([]*Resp, *Err, int, int), opts ...HandleOption) { + config := getConfigs(opts...) + registerEndpoint(r.Router.BasePath()+r.Path+pathString, "GET", new(Req), new(Resp), *config) r.Router.GET(r.Path+pathString, func(c *gin.Context) { params := r.extractRequestParams(c) limit, err := strconv.Atoi(c.DefaultQuery("limit", "30")) if err != nil { - c.JSON(http.StatusBadRequest, &ProcessorError{ + c.JSON(http.StatusBadRequest, &Err{ ErrCode: "INVALID_LIMIT_ERROR", ErrReason: "Bad Request", StatusCode: http.StatusBadRequest, @@ -143,7 +197,7 @@ func (r *APIEndpoint[Req, Resp]) HandleList(pathString string, requestProcessor offset, err := strconv.Atoi(c.DefaultQuery("offset", "0")) // default offset is 0 if err != nil { - c.JSON(http.StatusBadRequest, &ProcessorError{ + c.JSON(http.StatusBadRequest, &Err{ ErrCode: "INVALID_OFFSET_ERROR", ErrReason: "Bad Request", StatusCode: http.StatusBadRequest, @@ -234,11 +288,88 @@ func GetAuthenticationHeader(req *RequestParams) (string, error) { } return authentication, nil } -func ErrorBuilder(errCode, errReason, message string, statusCode int) *ProcessorError { - return &ProcessorError{ +func ErrorBuilder(errCode, errReason, message string, statusCode int) *Err { + return &Err{ ErrCode: errCode, ErrReason: errReason, StatusCode: statusCode, Message: message, } } + +func registerEndpoint(path, method string, request, response interface{}, config EndpointConfigs) { + // Check if the endpoint already exists + if endpoint, ok := Endpoints[path]; ok { + // Check if the method already exists for this endpoint + for _, m := range endpoint.Methods { + if m.HTTPMethod == method { + // Method already exists, return or handle as appropriate + return + } + } + // Method does not exist, add it to the endpoint's methods + endpoint.Methods = append(endpoint.Methods, Method{ + HTTPMethod: method, + Request: request, + Response: response, + Description: config.Descriptions, + Tags: config.Tags, + Summery: config.Name, + Configs: config, + }) + } else { + // If the endpoint doesn't exist, create a new entry with the method + Endpoints[path] = &Endpoint{ + Path: path, + Methods: []Method{ + { + HTTPMethod: method, + Request: request, + Response: response, + Description: config.Descriptions, + Tags: config.Tags, + Summery: config.Name, + Configs: config, + }, + }, + } + } +} + +type HandleOption func(*EndpointConfigs) + +func WithName(name string) HandleOption { + return func(c *EndpointConfigs) { + c.Name = name + } +} + +func WithURI(uri string) HandleOption { + return func(c *EndpointConfigs) { + c.Uri = uri + } +} + +func WithTags(tags []string) HandleOption { + return func(c *EndpointConfigs) { + c.Tags = tags + } +} + +func WithDescriptions(descriptions string) HandleOption { + return func(c *EndpointConfigs) { + c.Descriptions = descriptions + } +} + +func WithAllowedHeaders(headers []AllowedFields) HandleOption { + return func(c *EndpointConfigs) { + c.AllowedHeaders = headers + } +} + +func WithAllowedParams(params []AllowedFields) HandleOption { + return func(c *EndpointConfigs) { + c.AllowedParams = params + } +} diff --git a/router/endpoint_test.go b/router/endpoint_test.go index 6c56c77..9ceec17 100644 --- a/router/endpoint_test.go +++ b/router/endpoint_test.go @@ -34,7 +34,7 @@ func TestCreate(t *testing.T) { } // Define a request processor function that returns a response body and a nil error - requestProcessor := func(req Req, _ *RequestParams) (*ProcessorError, *Req) { + requestProcessor := func(req Req, _ *RequestParams) (*Err, *Req) { foo := "bar" return nil, &Req{ Foo: &foo, @@ -43,7 +43,7 @@ func TestCreate(t *testing.T) { } // Register the request processor function with the APIEndpoint instance - e.HandleCreate("", requestProcessor) + e.HandleCreate("", EndpointConfigs{}, requestProcessor) // HandleCreate a mock HTTP POST request req, err := http.NewRequest(http.MethodPost, "/foo", bytes.NewBuffer([]byte(`{"foo":"bar","baz":123}`))) @@ -86,7 +86,7 @@ func TestShouldUpdate(t *testing.T) { } // Define a request processor function that returns a response body and a nil error - requestProcessor := func(id string, req Req, _ *RequestParams) (*ProcessorError, *Resp) { + requestProcessor := func(id string, req Req, _ *RequestParams) (*Err, *Resp) { return nil, &Resp{ Foo: "bar", Baz: 123, @@ -138,7 +138,7 @@ func TestShouldUpdateWithWrongContentType(t *testing.T) { } // Define a request processor function that returns a response body and a nil error - requestProcessor := func(id string, req Req, _ *RequestParams) (*ProcessorError, *Resp) { + requestProcessor := func(id string, req Req, _ *RequestParams) (*Err, *Resp) { return nil, &Resp{ Foo: "bar", Baz: 123, @@ -180,7 +180,7 @@ func TestShouldUpdateWithWrongJson(t *testing.T) { } // Define a request processor function that returns a response body and a nil error - requestProcessor := func(id string, req Req, _ *RequestParams) (*ProcessorError, *Resp) { + requestProcessor := func(id string, req Req, _ *RequestParams) (*Err, *Resp) { return nil, &Resp{ Foo: "bar", Baz: 123, @@ -221,8 +221,8 @@ func TestShouldNotUpdateWithProccesorErr(t *testing.T) { } // Define a request processor function that returns a response body and a nil error - requestProcessor := func(id string, req Req, _ *RequestParams) (*ProcessorError, *Resp) { - return &ProcessorError{ + requestProcessor := func(id string, req Req, _ *RequestParams) (*Err, *Resp) { + return &Err{ StatusCode: 500, }, nil } @@ -261,7 +261,7 @@ func TestShouldTryCreateWithWrongJsonFormat(t *testing.T) { } // Define a request processor function that returns a response body and a nil error - requestProcessor := func(req Req, _ *RequestParams) (*ProcessorError, *Resp) { + requestProcessor := func(req Req, _ *RequestParams) (*Err, *Resp) { return nil, &Resp{ Foo: "bar", Baz: 123, @@ -269,7 +269,7 @@ func TestShouldTryCreateWithWrongJsonFormat(t *testing.T) { } // Register the request processor function with the APIEndpoint instance - e.HandleCreate("", requestProcessor) + e.HandleCreate("", EndpointConfigs{}, requestProcessor) // HandleCreate a mock HTTP POST request req, err := http.NewRequest(http.MethodPost, "/foo", @@ -304,7 +304,7 @@ func TestShouldTryCreateWithEmptyFIeld(t *testing.T) { } // Define a request processor function that returns a response body and a nil error - requestProcessor := func(req Req, _ *RequestParams) (*ProcessorError, *Resp) { + requestProcessor := func(req Req, _ *RequestParams) (*Err, *Resp) { return nil, &Resp{ Foo: "bar", Baz: 123, @@ -312,7 +312,7 @@ func TestShouldTryCreateWithEmptyFIeld(t *testing.T) { } // Register the request processor function with the APIEndpoint instance - e.HandleCreate("", requestProcessor) + e.HandleCreate("", EndpointConfigs{}, requestProcessor) // HandleCreate a mock HTTP POST request req, err := http.NewRequest(http.MethodPost, "/foo", @@ -346,7 +346,7 @@ func TestShouldTryCreateWithExtraField(t *testing.T) { } // Define a request processor function that returns a response body and a nil error - requestProcessor := func(req Req, _ *RequestParams) (*ProcessorError, *Resp) { + requestProcessor := func(req Req, _ *RequestParams) (*Err, *Resp) { return nil, &Resp{ Foo: "bar", Baz: 123, @@ -354,7 +354,7 @@ func TestShouldTryCreateWithExtraField(t *testing.T) { } // Register the request processor function with the APIEndpoint instance - e.HandleCreate("", requestProcessor) + e.HandleCreate("", EndpointConfigs{}, requestProcessor) // HandleCreate a mock HTTP POST request req, err := http.NewRequest(http.MethodPost, "/foo", @@ -388,8 +388,8 @@ func TestShouldTryCreateWithRequestProcessorError(t *testing.T) { } // Define a request processor function that returns a response body and a nil error - requestProcessor := func(req Req, _ *RequestParams) (*ProcessorError, *Resp) { - perr := &ProcessorError{ + requestProcessor := func(req Req, _ *RequestParams) (*Err, *Resp) { + perr := &Err{ err: errors.New("i'm an error"), StatusCode: 500, ErrCode: "ERR_CODE", @@ -402,7 +402,7 @@ func TestShouldTryCreateWithRequestProcessorError(t *testing.T) { } // Register the request processor function with the APIEndpoint instance - e.HandleCreate("", requestProcessor) + e.HandleCreate("", EndpointConfigs{}, requestProcessor) // HandleCreate a mock HTTP POST request req, err := http.NewRequest(http.MethodPost, "/foo", @@ -436,7 +436,7 @@ func TestCreateWithInvalidBody(t *testing.T) { } // Define a request processor function that returns a response body and a nil error - requestProcessor := func(req Req, _ *RequestParams) (*ProcessorError, *Resp) { + requestProcessor := func(req Req, _ *RequestParams) (*Err, *Resp) { return nil, &Resp{ Foo: "bar", Baz: 123, @@ -444,7 +444,7 @@ func TestCreateWithInvalidBody(t *testing.T) { } // Register the request processor function with the APIEndpoint instance - e.HandleCreate("", requestProcessor) + e.HandleCreate("", EndpointConfigs{}, requestProcessor) // HandleCreate a mock HTTP POST request req, err := http.NewRequest(http.MethodPost, "/foo", bytes.NewBuffer([]byte(`{"fo}`))) @@ -477,7 +477,7 @@ func TestCreateWithEmptyBody(t *testing.T) { } // Define a request processor function that returns a response body and a nil error - requestProcessor := func(req Req, _ *RequestParams) (*ProcessorError, *Resp) { + requestProcessor := func(req Req, _ *RequestParams) (*Err, *Resp) { return nil, &Resp{ Foo: "bar", Baz: 123, @@ -485,7 +485,7 @@ func TestCreateWithEmptyBody(t *testing.T) { } // Register the request processor function with the APIEndpoint instance - e.HandleCreate("", requestProcessor) + e.HandleCreate("", EndpointConfigs{}, requestProcessor) // HandleCreate a mock HTTP POST request req, err := http.NewRequest(http.MethodPost, "/foo", bytes.NewBuffer([]byte(`{}`))) @@ -517,7 +517,7 @@ func TestCreateWithWrongContentType(t *testing.T) { } // Define a request processor function that returns a response body and a nil error - requestProcessor := func(req Req, _ *RequestParams) (*ProcessorError, *Resp) { + requestProcessor := func(req Req, _ *RequestParams) (*Err, *Resp) { return nil, &Resp{ Foo: "bar", Baz: 123, @@ -525,7 +525,7 @@ func TestCreateWithWrongContentType(t *testing.T) { } // Register the request processor function with the APIEndpoint instance - e.HandleCreate("", requestProcessor) + e.HandleCreate("", EndpointConfigs{}, requestProcessor) // HandleCreate a mock HTTP POST request req, err := http.NewRequest(http.MethodPost, "/foo", bytes.NewBuffer([]byte(`{"foo":"bar","baz":123}`))) @@ -556,7 +556,7 @@ func TestRead(t *testing.T) { } // Define a request processor function that returns a response body and a nil error - requestProcessor := func(values *RequestParams) (*ProcessorError, *Resp) { + requestProcessor := func(values *RequestParams) (*Err, *Resp) { return nil, &Resp{ Foo: "bar", Baz: 123, @@ -607,7 +607,7 @@ func TestReadWithoutFields(t *testing.T) { } // Define a request processor function that returns a response body and a nil error - requestProcessor := func(values *RequestParams) (*ProcessorError, *Resp) { + requestProcessor := func(values *RequestParams) (*Err, *Resp) { return nil, &Resp{ Foo: "bar", Baz: 123, @@ -657,8 +657,8 @@ func TestReadWithoutProcessorError(t *testing.T) { } // Define a request processor function that returns a response body and a nil error - requestProcessor := func(values *RequestParams) (*ProcessorError, *Resp) { - return &ProcessorError{ + requestProcessor := func(values *RequestParams) (*Err, *Resp) { + return &Err{ StatusCode: 500, ErrCode: "someErrCode", ErrReason: "NoReason", @@ -706,7 +706,7 @@ func TestReadWithInvalidFIelds(t *testing.T) { } // Define a request processor function that returns a response body and a nil error - requestProcessor := func(values *RequestParams) (*ProcessorError, *Resp) { + requestProcessor := func(values *RequestParams) (*Err, *Resp) { return nil, &Resp{ Foo: "bar", Baz: 123, diff --git a/router/field_selector.go b/router/field_selector.go index d291d70..2c2bd03 100644 --- a/router/field_selector.go +++ b/router/field_selector.go @@ -2,7 +2,7 @@ package router type fieldType map[string]interface{} -func fieldSelector(fields []string, data fieldType) (fieldType, *ProcessorError) { +func fieldSelector(fields []string, data fieldType) (fieldType, *Err) { if len(fields) == 0 { return data, nil } @@ -13,7 +13,7 @@ func fieldSelector(fields []string, data fieldType) (fieldType, *ProcessorError) result[field] = value continue } - perr := ProcessorError{ + perr := Err{ ErrCode: "INVALID_FIELD_ERROR", ErrReason: "The field <" + field + "> does not exist", Message: "Invalid field <" + field + ">", diff --git a/router/processor_err.go b/router/processor_err.go index 848883d..4434185 100644 --- a/router/processor_err.go +++ b/router/processor_err.go @@ -2,7 +2,7 @@ package router import "errors" -type ProcessorError struct { +type Err struct { StatusCode int ErrCode string ErrReason string @@ -10,7 +10,7 @@ type ProcessorError struct { err error } -func (e *ProcessorError) Error() string { +func (e *Err) Error() string { e.err = errors.New(e.Message) return e.err.Error() } diff --git a/router/processor_err_test.go b/router/processor_err_test.go index 8bd06dd..9ca2cc8 100644 --- a/router/processor_err_test.go +++ b/router/processor_err_test.go @@ -6,7 +6,7 @@ import ( ) func TestProcessorError_Error(t *testing.T) { - err := &ProcessorError{ + err := &Err{ StatusCode: 500, ErrCode: "INTERNAL_ERROR", ErrReason: "An internal error occurred while processing the request", diff --git a/router/validation_test.go b/router/validation_test.go index 1c3b5de..028b2e3 100644 --- a/router/validation_test.go +++ b/router/validation_test.go @@ -53,8 +53,8 @@ func TestInput(t *testing.T) { func TestProcessorErr(t *testing.T) { validation := Validation{} - // Test with a ProcessorError struct - perr := ProcessorError{ + // Test with a Err struct + perr := Err{ ErrCode: "123", ErrReason: "Test Error", Message: "This is a test error message", diff --git a/router/validations.go b/router/validations.go index 9b0c5ab..c65a812 100644 --- a/router/validations.go +++ b/router/validations.go @@ -20,7 +20,7 @@ func (va *Validation) contentType() (int, Error) { } -func (va *Validation) ProcessorErr(perr *ProcessorError) (int, Error) { +func (va *Validation) ProcessorErr(perr *Err) (int, Error) { exp := Error{ Code: perr.ErrCode, Reason: perr.ErrReason, diff --git a/swagger.go b/swagger.go new file mode 100644 index 0000000..29c1544 --- /dev/null +++ b/swagger.go @@ -0,0 +1,443 @@ +package worx + +import ( + "errors" + "fmt" + "github.com/grahms/worx/router" + "reflect" + "regexp" + "strings" +) + +type Map map[string]interface{} +type OpenAPI struct { + swagger Map + title string + version string + description string + paths Map + endpoints map[string]*router.Endpoint +} + +func New(title, version, description string) *OpenAPI { + swagger := make(Map) + swagger["openapi"] = "3.0.0" + swagger["info"] = Map{ + "title": title, + "version": version, + "description": description, + } + return &OpenAPI{ + swagger: swagger, + title: title, + version: version, + description: description, + paths: make(Map), + } +} + +func (o *OpenAPI) SetEndpoints(endpoints map[string]*router.Endpoint) *OpenAPI { + o.endpoints = endpoints + return o +} + +func (o *OpenAPI) Build() (Map, error) { + if len(o.endpoints) == 0 { + return nil, errors.New("no endpoints provided") + } + + for _, endpoint := range o.endpoints { + o.paths[endpoint.Path] = o.buildPathItem(endpoint) + } + o.swagger["paths"] = o.paths + return o.swagger, nil +} + +func (o *OpenAPI) buildPathItem(endpoint *router.Endpoint) Map { + pathItem := make(Map) + path := endpoint.Path + // Find all dynamic segments in the path and replace them with {param}/ + re := regexp.MustCompile(`/:(\w+)(/|$)`) + path = re.ReplaceAllString(path, "/{$1}/") + + // Build path parameters for each dynamic segment + re = regexp.MustCompile(`{(\w+)}/`) + matches := re.FindAllStringSubmatch(path, -1) + for _, match := range matches { + paramName := match[1] + if pathItem["parameters"] == nil { + pathItem["parameters"] = []Map{} + } + // Add path parameter to the path parameters map + pathItemParams := make(Map) + pathItemParams["name"] = paramName + pathItemParams["in"] = "path" + pathItemParams["required"] = true + pathItemParams["schema"] = Map{"type": "string"} + pathItemParams["description"] = fmt.Sprintf("Path parameter %s", paramName) + pathItem["parameters"] = append(pathItem["parameters"].([]Map), pathItemParams) + } + + for _, method := range endpoint.Methods { + pathItem[strings.ToLower(method.HTTPMethod)] = o.buildOperation(method) + } + + o.paths[path] = pathItem + return pathItem +} + +func (o *OpenAPI) buildOperation(method router.Method) Map { + operation := Map{ + "responses": Map{ + "200": o.buildResponse(), + }, + } + + if method.HTTPMethod != "GET" && method.Request != nil { + operation["requestBody"] = o.buildRequestBody(method.Request) + } + + if method.Response != nil { + schema := Schema{} + operation["responses"].(Map)["200"].(Map)["content"].(Map)["application/json"].(Map)["schema"] = schema.Build(method.Response, "response") + } + operation["tags"] = method.Configs.Tags + operation["description"] = method.Description + operation["summary"] = method.Configs.Name + + parameters := o.buildParameters(method.Configs.AllowedHeaders, method.Configs.AllowedParams) + if len(parameters) > 0 { + operation["parameters"] = parameters + } + + return operation +} + +func (o *OpenAPI) buildResponse() Map { + return Map{ + "description": "Successful operation", + "content": Map{ + "application/json": Map{ + "schema": Map{ + "type": "object", + }, + }, + }, + } +} + +func (o *OpenAPI) buildRequestBody(request interface{}) Map { + s := Schema{} + requestSchema := s.Build(request, "request") + return Map{ + "required": true, + "content": Map{ + "application/json": Map{ + "schema": requestSchema, + }, + }, + } +} + +func (o *OpenAPI) buildParameters(headers []router.AllowedFields, queryParams []router.AllowedFields) []Map { + parameters := make([]Map, 0) + + for _, header := range headers { + parameters = append(parameters, o.buildHeaderParameter(header)) + } + + for _, param := range queryParams { + parameters = append(parameters, o.buildQueryParamParameter(param)) + } + + return parameters +} + +func (o *OpenAPI) buildHeaderParameter(header router.AllowedFields) Map { + return Map{ + "in": "header", + "name": header.Name, + "description": header.Description, + "required": header.Required, + "schema": Map{ + "type": "string", + }, + } +} + +func (o *OpenAPI) buildQueryParamParameter(param router.AllowedFields) Map { + return Map{ + "in": "query", + "name": param.Name, + "description": param.Description, + "required": param.Required, + "schema": Map{ + "type": "string", + }, + } +} + +func isPtr(value reflect.Value) bool { + return value.Kind() == reflect.Ptr +} + +func isList(value reflect.Type) bool { + return value.Kind() == reflect.Slice || value.Kind() == reflect.Array +} + +func (sc *Schema) buildSchemaFromStructByType(structType reflect.Type, schemaType string) map[string]interface{} { + schema := make(map[string]interface{}) + + switch schemaType { + case "object": + schema["type"] = "object" + properties := make(map[string]interface{}) + required := make([]string, 0) + + // Iterate over the fields of the struct + for i := 0; i < structType.NumField(); i++ { + field := structType.Field(i) + fieldName := field.Name + + // Check if the field is required based on struct tags + if jsonTag := field.Tag.Get("json"); jsonTag != "" { + fieldName = jsonTag + } + if bindingTag := field.Tag.Get("binding"); bindingTag == "required" { + required = append(required, fieldName) + } + + // Determine the schema for the field + fieldSchema := sc.buildFieldFromSchema(field, schemaType) + + // Add the field schema to the properties map + properties[fieldName] = fieldSchema + } + + // Add properties and required fields to the schema + schema["properties"] = properties + if len(required) > 0 { + schema["required"] = required + } + + case "array": + schema["type"] = "array" + schema["items"] = sc.buildSchemaFromStructByType(structType, "object") + + default: + // Unsupported schema type + fmt.Println("Unsupported schema type:", schemaType) + } + + return schema +} + +type Schema struct{} + +func (sc *Schema) Build(input interface{}, structType string) map[string]interface{} { + + if reflect.TypeOf(input).Kind() == reflect.Ptr { + input = reflect.ValueOf(input).Elem().Interface() + } + t := reflect.TypeOf(input) + if stype := sc.getStructTypeFromList(t); stype != nil { + return sc.buildSchemaFromStructByType(stype, "array") + } + + schema := make(map[string]interface{}) + schema["type"] = "object" + + properties := make(map[string]interface{}) + required := make([]string, 0) + + for i := 0; i < t.NumField(); i++ { + field := t.Field(i) + fieldName := sc.getFieldName(field) + if len(fieldName) == 0 { + continue + } + fieldName = field.Tag.Get("json") + required = sc.checkRequired(field, required) + if len(field.Tag.Get("binding")) > 0 { + binding := field.Tag.Get("binding") + switch binding { + case "ignore": + if structType == "request" { + continue + } + + } + } + + // Check if the field is a list or a nested struct + fieldValue := reflect.ValueOf(input).Field(i) + if isList(fieldValue.Type()) || fieldValue.Kind() == reflect.Struct { + properties[fieldName] = sc.buildNestedSchema(fieldValue, structType) + } else { + properties[fieldName] = sc.buildFieldFromSchema(field, structType) + } + } + + schema["properties"] = properties + + // Add required fields to the schema if any + if len(required) > 0 { + schema["required"] = required + } + + return schema + +} +func (sc *Schema) buildNestedSchema(fieldValue reflect.Value, structType string) map[string]interface{} { + if isPtr(fieldValue) { + sc.buildNestedSchema(fieldValue.Elem(), structType) + } + ftype := fieldValue.Type() + if isList(ftype) { + listSchema := map[string]interface{}{ + "type": "array", + "items": sc.Build(fieldValue.Type(), structType), + } + return listSchema + } else if fieldValue.Kind() == reflect.Struct { + return sc.Build(fieldValue.Interface(), structType) + } + // Otherwise, return an empty schema + return map[string]interface{}{} +} + +func (sc *Schema) getFieldName(field reflect.StructField) string { + if len(field.Tag.Get("json")) > 0 { + return field.Tag.Get("json") + } + return field.Name +} + +func (sc *Schema) checkRequired(field reflect.StructField, required []string) []string { + if binding := field.Tag.Get("binding"); len(binding) > 0 && binding == "required" { + required = append(required, sc.getFieldName(field)) + } + return required +} + +func (sc *Schema) buildSchemaForString(field reflect.StructField) map[string]interface{} { + schema := map[string]interface{}{ + "type": "string", + } + if enums := field.Tag.Get("enums"); len(enums) > 0 { + enumValues := strings.Split(strings.TrimSpace(enums), ",") + schema["enum"] = enumValues + } + return schema +} + +func (sc *Schema) buildSchemaForInteger() map[string]interface{} { + return map[string]interface{}{ + "type": "integer", + } +} + +func (sc *Schema) buildSchemaForNumber() map[string]interface{} { + return map[string]interface{}{ + "type": "number", + } +} + +func (sc *Schema) buildSchemaForStruct(structValue interface{}, structType string) map[string]interface{} { + // Handle nested structs + return sc.Build(structValue, structType) +} + +func (sc *Schema) getStructTypeFromList(listType reflect.Type) reflect.Type { + + if listType.Kind() == reflect.Slice || listType.Kind() == reflect.Array { + // Get the element type of the slice/array + elementType := listType.Elem() + // If the element type is a struct, return it + if elementType.Kind() == reflect.Struct { + return elementType + } + } + return nil +} + +// func (sc *Schema) buildSchemaForArray(fieldType reflect.Type) map[string]interface{} { +// return map[string]interface{}{ +// "type": "array", +// "items": sc.buildFieldFromSchema(reflect.New(fieldType).Elem().Interface()), +// } +// } +func (sc *Schema) buildFieldFromSchema(field reflect.StructField, inputType string) map[string]interface{} { + fieldType := field.Type + if fieldType.Kind() == reflect.Ptr { + fieldType = fieldType.Elem() + } + + var jsonType string + switch fieldType.Kind() { + case reflect.String: + jsonType = "string" + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, + reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + jsonType = "integer" + case reflect.Float32, reflect.Float64: + jsonType = "number" + case reflect.Struct: + // If the field is a struct, recursively build schema for it + return sc.Build(reflect.New(fieldType), inputType) + case reflect.Bool: + jsonType = "boolean" + case reflect.Slice, reflect.Array: + // Get the element type of the slice/array + elementType := fieldType.Elem() + // If the element type is a struct, return it + if elementType.Kind() == reflect.Struct { + return sc.Build(reflect.New(fieldType).Elem().Interface(), inputType) + } + + default: + jsonType = "object" + } + + schema := map[string]interface{}{ + "type": jsonType, + } + + // Check if the field has enums + if enums := field.Tag.Get("enums"); len(enums) > 0 { + enumValues := strings.Split(strings.TrimSpace(enums), ",") + schema["enum"] = enumValues + } + + return schema +} + +func (sc *Schema) buildSchemaForObject() map[string]interface{} { + return map[string]interface{}{ + "type": "object", + } +} + +var swagTempl = ` + + + + + Swagger UI + + + +
+ + + + +` diff --git a/worx.go b/worx.go index 33d5045..ebf6093 100644 --- a/worx.go +++ b/worx.go @@ -1,26 +1,31 @@ package worx import ( + "bytes" "encoding/json" "fmt" "github.com/gin-contrib/cors" "github.com/gin-gonic/gin" "github.com/grahms/worx/router" + "html/template" + "net/http" "time" ) type Application struct { - name string - path string - router *gin.RouterGroup - engine *gin.Engine + name string + path string + router *gin.RouterGroup + engine *gin.Engine + version string + description string } func NewRouter[In, Out any](app *Application, path string) *router.APIEndpoint[In, Out] { return router.New[In, Out](path, app.router.Group("")) } -func NewApplication(path, name string, middlewares ...gin.HandlerFunc) *Application { +func NewApplication(path, name, version, description string, middlewares ...gin.HandlerFunc) *Application { r := Engine() config := cors.DefaultConfig() @@ -37,16 +42,27 @@ func NewApplication(path, name string, middlewares ...gin.HandlerFunc) *Applicat noRoute(r) g := r.Group(path) return &Application{ - name: name, - path: path, - router: g, - engine: r, + name: name, + path: path, + router: g, + engine: r, + version: version, + description: description, } } type _ any func (a *Application) Run(address string) error { + + s, err := New(a.name, a.version, a.description).SetEndpoints(router.Endpoints).Build() + if err != nil { + panic(err) + } + bJ, _ := json.Marshal(s) + + a.router.GET("/spec", RenderSwagg(string(bJ))) // Serve swagger ui + return a.engine.Run(address) } func noRoute(r *gin.Engine) { @@ -161,3 +177,26 @@ var defaultLogFormatter = func(param gin.LogFormatterParams) string { } return string(logJSON) + "\n" } + +func RenderSwagg(spec string) func(c *gin.Context) { + return func(c *gin.Context) { + + tmplData := struct { + SwaggerJSON string + }{ + SwaggerJSON: spec, + } + + t := template.Must(template.New("swagger").Parse(swagTempl)) + + var buf bytes.Buffer + if err := t.Execute(&buf, tmplData); err != nil { + c.String(http.StatusInternalServerError, "Failed to render Swagger UI") + return + } + + c.Header("Content-Type", "text/html") + c.String(http.StatusOK, buf.String()) + } + +}