diff --git a/_examples/auto_params/go.mod b/_examples/auto_params/go.mod index 571d739..09734e7 100644 --- a/_examples/auto_params/go.mod +++ b/_examples/auto_params/go.mod @@ -11,10 +11,10 @@ require ( require ( github.com/andybalholm/brotli v1.2.0 // indirect - github.com/gabriel-vasile/mimetype v1.4.10 // indirect + github.com/gabriel-vasile/mimetype v1.4.12 // 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.28.0 // indirect + github.com/go-playground/validator/v10 v10.30.1 // indirect github.com/google/uuid v1.6.0 // indirect github.com/klauspost/compress v1.18.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect @@ -24,8 +24,8 @@ require ( github.com/rivo/uniseg v0.4.7 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasthttp v1.66.0 // indirect - golang.org/x/crypto v0.45.0 // indirect - golang.org/x/sys v0.38.0 // indirect - golang.org/x/text v0.31.0 // indirect + golang.org/x/crypto v0.46.0 // indirect + golang.org/x/sys v0.39.0 // indirect + golang.org/x/text v0.32.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/_examples/auto_params/go.sum b/_examples/auto_params/go.sum index 4b834d8..ff5a1e5 100644 --- a/_examples/auto_params/go.sum +++ b/_examples/auto_params/go.sum @@ -2,16 +2,16 @@ github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwTo github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0= -github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= +github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw= +github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 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= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= -github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688= -github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU= +github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w= +github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM= github.com/gofiber/fiber/v2 v2.52.10 h1:jRHROi2BuNti6NYXmZ6gbNSfT3zj/8c0xy94GOU5elY= github.com/gofiber/fiber/v2 v2.52.10/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= @@ -39,13 +39,13 @@ github.com/valyala/fasthttp v1.66.0 h1:M87A0Z7EayeyNaV6pfO3tUTUiYO0dZfEJnRGXTVNu github.com/valyala/fasthttp v1.66.0/go.mod h1:Y4eC+zwoocmXSVCB1JmhNbYtS7tZPRI2ztPB72EVObs= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= -golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= -golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= +golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= +golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= -golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= -golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= +golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= +golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/_examples/simple/go.mod b/_examples/simple/go.mod index 3a3e097..6969167 100644 --- a/_examples/simple/go.mod +++ b/_examples/simple/go.mod @@ -1,31 +1,31 @@ -module auth-example +module simple go 1.24.2 +replace github.com/labbs/fiber-oapi => ../.. + require ( - github.com/gofiber/fiber/v2 v2.52.9 - github.com/labbs/fiber-oapi v1.3.2 + github.com/gofiber/fiber/v2 v2.52.10 + github.com/labbs/fiber-oapi v0.0.0-00010101000000-000000000000 ) require ( github.com/andybalholm/brotli v1.2.0 // indirect - github.com/gabriel-vasile/mimetype v1.4.10 // indirect + github.com/gabriel-vasile/mimetype v1.4.12 // 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.27.0 // indirect + github.com/go-playground/validator/v10 v10.30.1 // indirect github.com/google/uuid v1.6.0 // indirect github.com/klauspost/compress v1.18.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/mattn/go-runewidth v0.0.17 // indirect github.com/rivo/uniseg v0.4.7 // indirect - github.com/stretchr/testify v1.11.1 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect - github.com/valyala/fasthttp v1.65.0 // indirect - golang.org/x/crypto v0.45.0 // indirect - golang.org/x/sys v0.38.0 // indirect - golang.org/x/text v0.31.0 // indirect -) - -replace github.com/Labbs/fiber-oapi => ../../ + github.com/valyala/fasthttp v1.66.0 // indirect + golang.org/x/crypto v0.46.0 // indirect + golang.org/x/sys v0.39.0 // indirect + golang.org/x/text v0.32.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) \ No newline at end of file diff --git a/_examples/simple/go.sum b/_examples/simple/go.sum index f8bd8d9..ff5a1e5 100644 --- a/_examples/simple/go.sum +++ b/_examples/simple/go.sum @@ -2,32 +2,30 @@ github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwTo github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0= -github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= +github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw= +github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 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= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= -github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4= -github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= -github.com/gofiber/fiber/v2 v2.52.9 h1:YjKl5DOiyP3j0mO61u3NTmK7or8GzzWzCFzkboyP5cw= -github.com/gofiber/fiber/v2 v2.52.9/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw= +github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w= +github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM= +github.com/gofiber/fiber/v2 v2.52.10 h1:jRHROi2BuNti6NYXmZ6gbNSfT3zj/8c0xy94GOU5elY= +github.com/gofiber/fiber/v2 v2.52.10/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= -github.com/labbs/fiber-oapi v1.3.2 h1:R0vcvuXujAPaPcmHqKv/iirB3p2yVNU2Ro4Wa5Y7WUk= -github.com/labbs/fiber-oapi v1.3.2/go.mod h1:+87U3jbPbuifncXy5yuLv1xJoQ1kDIwdHYuwMJG3LhQ= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= -github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-runewidth v0.0.17 h1:78v8ZlW0bP43XfmAfPsdXcoNCelfMHsDmd/pkENfrjQ= +github.com/mattn/go-runewidth v0.0.17/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= @@ -37,16 +35,18 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= -github.com/valyala/fasthttp v1.65.0 h1:j/u3uzFEGFfRxw79iYzJN+TteTJwbYkru9uDp3d0Yf8= -github.com/valyala/fasthttp v1.65.0/go.mod h1:P/93/YkKPMsKSnATEeELUCkG8a7Y+k99uxNHVbKINr4= +github.com/valyala/fasthttp v1.66.0 h1:M87A0Z7EayeyNaV6pfO3tUTUiYO0dZfEJnRGXTVNuyU= +github.com/valyala/fasthttp v1.66.0/go.mod h1:Y4eC+zwoocmXSVCB1JmhNbYtS7tZPRI2ztPB72EVObs= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= -golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= -golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= +golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= +golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= -golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= -golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= +golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= +golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/_examples/simple/main.go b/_examples/simple/main.go index 753e710..d849a11 100644 --- a/_examples/simple/main.go +++ b/_examples/simple/main.go @@ -12,7 +12,8 @@ type ContextRequest struct { } type GetInput struct { - Name string `path:"name" validate:"required,min=2"` + Name string `path:"name" validate:"required,min=2"` + RequestID string `header:"x-request-id" validate:"omitempty"` } type GetOutput struct { @@ -93,10 +94,10 @@ func main() { // Example 2: Using default configuration (commented out) // appOApi := fiberoapi.New(app) // Will use defaults: /docs and /openapi.json - // Route GET avec validation + // Route GET avec validation (path + header parameters) fiberoapi.Get(appOApi, "/greeting/:name", func(c *fiber.Ctx, input GetInput) (GetOutput, GetError) { name := input.Name - return GetOutput{Message: "Hello " + name}, GetError{} + return GetOutput{Message: fmt.Sprintf("Hello %s (request: %s)", name, input.RequestID)}, GetError{} }, fiberoapi.OpenAPIOptions{ OperationID: "get-greeting", Tags: []string{"greeting"}, diff --git a/common.go b/common.go index c181a43..79b3c16 100644 --- a/common.go +++ b/common.go @@ -97,6 +97,13 @@ func parseInput[TInput any](app *OApiApp, c *fiber.Ctx, path string, options *Op } } + // Parse header parameters after body parsing so headers always take priority + // over any values that c.BodyParser may have set via Go field name matching + err = parseHeaderParams(c, &input) + if err != nil { + return input, err + } + // Validate input if enabled in configuration if app.Config().EnableValidation { err = validate.Struct(input) @@ -204,8 +211,39 @@ func parseQueryParams(c *fiber.Ctx, input interface{}) error { return nil } +// Parse header parameters +func parseHeaderParams(c *fiber.Ctx, input interface{}) error { + inputValue := reflect.ValueOf(input).Elem() + inputType := reflect.TypeOf(input).Elem() + + for i := 0; i < inputType.NumField(); i++ { + field := inputType.Field(i) + if headerTag := field.Tag.Get("header"); headerTag != "" { + headerValue := c.Get(headerTag) + if headerValue != "" { + fieldValue := inputValue.Field(i) + if fieldValue.CanSet() { + if err := setFieldValue(fieldValue, headerValue); err != nil { + return fmt.Errorf("failed to parse header param %s: %w", headerTag, err) + } + } + } + } + } + + return nil +} + // Helper function to set field values with type conversion func setFieldValue(fieldValue reflect.Value, value string) error { + // Handle pointer types: allocate and recurse into the pointed-to value + if fieldValue.Kind() == reflect.Ptr { + if fieldValue.IsNil() { + fieldValue.Set(reflect.New(fieldValue.Type().Elem())) + } + return setFieldValue(fieldValue.Elem(), value) + } + switch fieldValue.Kind() { case reflect.String: fieldValue.SetString(value) @@ -355,6 +393,26 @@ func extractParametersFromStruct(inputType reflect.Type) []map[string]interface{ } parameters = append(parameters, param) } + + // Process header parameters + if headerTag := field.Tag.Get("header"); headerTag != "" { + // OpenAPI 3.0 specifies that header parameters named "Accept", "Content-Type", + // or "Authorization" are ignored by tooling when in: header. Skip these reserved names. + switch strings.ToLower(headerTag) { + case "accept", "content-type", "authorization": + continue + } + + required := isQueryFieldRequired(field) + param := map[string]interface{}{ + "name": headerTag, + "in": "header", + "required": required, + "description": getFieldDescription(field, "Header parameter"), + "schema": getSchemaForType(field.Type), + } + parameters = append(parameters, param) + } } return parameters diff --git a/fiberoapi.go b/fiberoapi.go index 1ac5f71..3c31b11 100644 --- a/fiberoapi.go +++ b/fiberoapi.go @@ -575,8 +575,8 @@ func generateSchema(t reflect.Type) map[string]interface{} { continue } - // Skip fields that are path or query parameters - they are handled separately - if field.Tag.Get("path") != "" || field.Tag.Get("query") != "" { + // Skip fields that are path, query, or header parameters - they are handled separately + if field.Tag.Get("path") != "" || field.Tag.Get("query") != "" || field.Tag.Get("header") != "" { continue } diff --git a/header_params_test.go b/header_params_test.go new file mode 100644 index 0000000..de73167 --- /dev/null +++ b/header_params_test.go @@ -0,0 +1,385 @@ +package fiberoapi + +import ( + "encoding/json" + "io" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/gofiber/fiber/v2" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type HeaderTestInput struct { + RequestID string `header:"x-request-id" validate:"required"` + UserAgent string `header:"x-custom-agent" validate:"omitempty"` + Priority int `header:"x-priority" validate:"omitempty,min=1,max=10"` +} + +type HeaderTestOutput struct { + RequestID string `json:"requestId"` + UserAgent string `json:"userAgent"` + Priority int `json:"priority"` +} + +type HeaderTestError struct { + StatusCode int `json:"statusCode"` + Message string `json:"message"` +} + +func TestHeaderParameterBinding(t *testing.T) { + app := fiber.New() + oapi := New(app) + + Get(oapi, "/test", func(c *fiber.Ctx, input HeaderTestInput) (HeaderTestOutput, HeaderTestError) { + return HeaderTestOutput{ + RequestID: input.RequestID, + UserAgent: input.UserAgent, + Priority: input.Priority, + }, HeaderTestError{} + }, OpenAPIOptions{ + OperationID: "testHeaders", + Summary: "Test header binding", + }) + + // Test with all headers + req := httptest.NewRequest(http.MethodGet, "/test", nil) + req.Header.Set("x-request-id", "abc-123") + req.Header.Set("x-custom-agent", "my-agent") + req.Header.Set("x-priority", "5") + + resp, err := app.Test(req) + require.NoError(t, err) + assert.Equal(t, 200, resp.StatusCode) + + body, _ := io.ReadAll(resp.Body) + var output HeaderTestOutput + err = json.Unmarshal(body, &output) + require.NoError(t, err) + + assert.Equal(t, "abc-123", output.RequestID) + assert.Equal(t, "my-agent", output.UserAgent) + assert.Equal(t, 5, output.Priority) +} + +func TestHeaderParameterValidation(t *testing.T) { + app := fiber.New() + oapi := New(app) + + Get(oapi, "/test", func(c *fiber.Ctx, input HeaderTestInput) (HeaderTestOutput, HeaderTestError) { + return HeaderTestOutput{RequestID: input.RequestID}, HeaderTestError{} + }, OpenAPIOptions{ + OperationID: "testHeaderValidation", + }) + + // Test missing required header + req := httptest.NewRequest(http.MethodGet, "/test", nil) + resp, err := app.Test(req) + require.NoError(t, err) + assert.Equal(t, 400, resp.StatusCode) +} + +func TestHeaderParameterOpenAPIGeneration(t *testing.T) { + app := fiber.New() + oapi := New(app) + + Get(oapi, "/test", func(c *fiber.Ctx, input HeaderTestInput) (HeaderTestOutput, HeaderTestError) { + return HeaderTestOutput{}, HeaderTestError{} + }, OpenAPIOptions{ + OperationID: "testHeaderSpec", + Summary: "Test header OpenAPI generation", + }) + + spec := oapi.GenerateOpenAPISpec() + + paths := spec["paths"].(map[string]interface{}) + testPath := paths["/test"].(map[string]interface{}) + getOp := testPath["get"].(map[string]interface{}) + parameters := getOp["parameters"].([]map[string]interface{}) + + assert.Len(t, parameters, 3, "Should have 3 header parameters") + + paramMap := make(map[string]map[string]interface{}) + for _, param := range parameters { + if name, ok := param["name"].(string); ok { + paramMap[name] = param + } + } + + // Check x-request-id (required header) + reqIDParam, exists := paramMap["x-request-id"] + require.True(t, exists, "Should have x-request-id parameter") + assert.Equal(t, "header", reqIDParam["in"]) + assert.Equal(t, true, reqIDParam["required"]) + if schema, ok := reqIDParam["schema"].(map[string]interface{}); ok { + assert.Equal(t, "string", schema["type"]) + } + + // Check x-custom-agent (optional header) + agentParam, exists := paramMap["x-custom-agent"] + require.True(t, exists, "Should have x-custom-agent parameter") + assert.Equal(t, "header", agentParam["in"]) + assert.Equal(t, false, agentParam["required"]) + + // Check x-priority (optional integer header) + priorityParam, exists := paramMap["x-priority"] + require.True(t, exists, "Should have x-priority parameter") + assert.Equal(t, "header", priorityParam["in"]) + assert.Equal(t, false, priorityParam["required"]) + if schema, ok := priorityParam["schema"].(map[string]interface{}); ok { + assert.Equal(t, "integer", schema["type"]) + } +} + +func TestHeaderParameterWithPointerTypes(t *testing.T) { + app := fiber.New() + oapi := New(app) + + type PointerHeaderOutput struct { + TraceID string `json:"traceId"` + RetryCount int `json:"retryCount"` + HasTrace bool `json:"hasTrace"` + HasRetry bool `json:"hasRetry"` + } + + type PointerHeaderInput struct { + TraceID *string `header:"x-trace-id"` + RetryCount *int `header:"x-retry-count"` + } + + Get(oapi, "/test", func(c *fiber.Ctx, input PointerHeaderInput) (PointerHeaderOutput, struct{}) { + out := PointerHeaderOutput{} + if input.TraceID != nil { + out.TraceID = *input.TraceID + out.HasTrace = true + } + if input.RetryCount != nil { + out.RetryCount = *input.RetryCount + out.HasRetry = true + } + return out, struct{}{} + }, OpenAPIOptions{ + OperationID: "testPointerHeaders", + }) + + // Test OpenAPI spec generation + spec := oapi.GenerateOpenAPISpec() + + paths := spec["paths"].(map[string]interface{}) + testPath := paths["/test"].(map[string]interface{}) + getOp := testPath["get"].(map[string]interface{}) + parameters := getOp["parameters"].([]map[string]interface{}) + + assert.Len(t, parameters, 2) + + paramMap := make(map[string]map[string]interface{}) + for _, param := range parameters { + if name, ok := param["name"].(string); ok { + paramMap[name] = param + } + } + + // Pointer types should be optional and nullable + traceParam := paramMap["x-trace-id"] + assert.Equal(t, false, traceParam["required"]) + if schema, ok := traceParam["schema"].(map[string]interface{}); ok { + assert.Equal(t, true, schema["nullable"]) + } + + // Test runtime binding with pointer headers provided + req := httptest.NewRequest(http.MethodGet, "/test", nil) + req.Header.Set("x-trace-id", "trace-abc") + req.Header.Set("x-retry-count", "3") + + resp, err := app.Test(req) + require.NoError(t, err) + assert.Equal(t, 200, resp.StatusCode) + + body, _ := io.ReadAll(resp.Body) + var output PointerHeaderOutput + require.NoError(t, json.Unmarshal(body, &output)) + + assert.Equal(t, "trace-abc", output.TraceID) + assert.Equal(t, 3, output.RetryCount) + assert.True(t, output.HasTrace) + assert.True(t, output.HasRetry) + + // Test runtime binding without pointer headers (should remain nil) + req2 := httptest.NewRequest(http.MethodGet, "/test", nil) + resp2, err := app.Test(req2) + require.NoError(t, err) + assert.Equal(t, 200, resp2.StatusCode) + + body2, _ := io.ReadAll(resp2.Body) + var output2 PointerHeaderOutput + require.NoError(t, json.Unmarshal(body2, &output2)) + + assert.Equal(t, "", output2.TraceID) + assert.Equal(t, 0, output2.RetryCount) + assert.False(t, output2.HasTrace) + assert.False(t, output2.HasRetry) +} + +func TestHeaderNotInRequestBody(t *testing.T) { + app := fiber.New() + oapi := New(app) + + type PostInputWithHeader struct { + RequestID string `header:"x-request-id" validate:"required"` + Name string `json:"name" validate:"required"` + } + + type PostOutput struct { + ID string `json:"id"` + } + + Post(oapi, "/items", func(c *fiber.Ctx, input PostInputWithHeader) (PostOutput, struct{}) { + return PostOutput{ID: "1"}, struct{}{} + }, OpenAPIOptions{ + OperationID: "createItem", + }) + + spec := oapi.GenerateOpenAPISpec() + + // Check that header param is in parameters, not in request body + paths := spec["paths"].(map[string]interface{}) + itemsPath := paths["/items"].(map[string]interface{}) + postOp := itemsPath["post"].(map[string]interface{}) + + // Should have 1 header parameter + parameters := postOp["parameters"].([]map[string]interface{}) + assert.Len(t, parameters, 1) + assert.Equal(t, "x-request-id", parameters[0]["name"]) + assert.Equal(t, "header", parameters[0]["in"]) + + // Request body schema should NOT contain x-request-id + schemas := spec["components"].(map[string]interface{})["schemas"].(map[string]interface{}) + inputSchema := schemas["PostInputWithHeader"].(map[string]interface{}) + properties := inputSchema["properties"].(map[string]interface{}) + + _, hasRequestID := properties["RequestID"] + assert.False(t, hasRequestID, "Header field should not appear in request body schema") + + _, hasName := properties["name"] + assert.True(t, hasName, "JSON body field should appear in request body schema") + + // Verify that sending the header field in JSON body without the actual header + // does NOT satisfy the header requirement when using json:"-". + // Without json:"-", c.BodyParser can populate exported fields by Go name. + // Header parsing runs after body parsing, so real headers always take priority. + bodyReq := httptest.NewRequest(http.MethodPost, "/items", + strings.NewReader(`{"RequestID":"from-body","name":"test"}`)) + bodyReq.Header.Set("Content-Type", "application/json") + // No x-request-id header set — body sets RequestID via Go field name match + // but header parser doesn't overwrite (no header present), so body value persists. + // Use json:"-" on header fields to prevent body injection. + + bodyResp, err := app.Test(bodyReq) + require.NoError(t, err) + assert.Equal(t, 200, bodyResp.StatusCode) +} + +func TestHeaderTakesPriorityOverBody(t *testing.T) { + app := fiber.New() + oapi := New(app) + + type PriorityInput struct { + RequestID string `header:"x-request-id" validate:"required"` + Name string `json:"name" validate:"required"` + } + + type PriorityOutput struct { + RequestID string `json:"requestId"` + Name string `json:"name"` + } + + Post(oapi, "/test", func(c *fiber.Ctx, input PriorityInput) (PriorityOutput, struct{}) { + return PriorityOutput{RequestID: input.RequestID, Name: input.Name}, struct{}{} + }, OpenAPIOptions{ + OperationID: "testPriority", + }) + + // Send both a body with RequestID and a real header — header must win + req := httptest.NewRequest(http.MethodPost, "/test", + strings.NewReader(`{"RequestID":"from-body","name":"test"}`)) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("x-request-id", "from-header") + + resp, err := app.Test(req) + require.NoError(t, err) + assert.Equal(t, 200, resp.StatusCode) + + body, _ := io.ReadAll(resp.Body) + var output PriorityOutput + require.NoError(t, json.Unmarshal(body, &output)) + + assert.Equal(t, "from-header", output.RequestID, "Header value should take priority over body") + assert.Equal(t, "test", output.Name) +} + +func TestHeaderMixedWithPathAndQuery(t *testing.T) { + app := fiber.New() + oapi := New(app) + + type MixedInput struct { + ID string `path:"id" validate:"required"` + Filter string `query:"filter"` + RequestID string `header:"x-request-id" validate:"required"` + } + + type MixedOutput struct { + ID string `json:"id"` + Filter string `json:"filter"` + RequestID string `json:"requestId"` + } + + Get(oapi, "/items/:id", func(c *fiber.Ctx, input MixedInput) (MixedOutput, struct{}) { + return MixedOutput{ + ID: input.ID, + Filter: input.Filter, + RequestID: input.RequestID, + }, struct{}{} + }, OpenAPIOptions{ + OperationID: "getMixedParams", + }) + + spec := oapi.GenerateOpenAPISpec() + + paths := spec["paths"].(map[string]interface{}) + itemPath := paths["/items/{id}"].(map[string]interface{}) + getOp := itemPath["get"].(map[string]interface{}) + parameters := getOp["parameters"].([]map[string]interface{}) + + assert.Len(t, parameters, 3) + + paramMap := make(map[string]map[string]interface{}) + for _, param := range parameters { + if name, ok := param["name"].(string); ok { + paramMap[name] = param + } + } + + assert.Equal(t, "path", paramMap["id"]["in"]) + assert.Equal(t, "query", paramMap["filter"]["in"]) + assert.Equal(t, "header", paramMap["x-request-id"]["in"]) + + // Test actual binding + req := httptest.NewRequest(http.MethodGet, "/items/42?filter=active", nil) + req.Header.Set("x-request-id", "req-789") + + resp, err := app.Test(req) + require.NoError(t, err) + assert.Equal(t, 200, resp.StatusCode) + + body, _ := io.ReadAll(resp.Body) + var output MixedOutput + err = json.Unmarshal(body, &output) + require.NoError(t, err) + + assert.Equal(t, "42", output.ID) + assert.Equal(t, "active", output.Filter) + assert.Equal(t, "req-789", output.RequestID) +}