package ginext import ( "context" "fmt" "github.com/gin-gonic/gin" "gogs.mikescher.com/BlackForestBytes/goext/exerr" json "gogs.mikescher.com/BlackForestBytes/goext/gojson" "gogs.mikescher.com/BlackForestBytes/goext/langext" "os" ) type cookieval struct { name string value string maxAge int path string domain string secure bool httpOnly bool } type headerval struct { Key string Val string } type HTTPResponse interface { Write(g *gin.Context) WithHeader(k string, v string) HTTPResponse WithCookie(name string, value string, maxAge int, path string, domain string, secure bool, httpOnly bool) HTTPResponse IsSuccess() bool } type InspectableHTTPResponse interface { HTTPResponse Statuscode() int BodyString(g *gin.Context) *string ContentType() string Headers() []string } type jsonHTTPResponse struct { statusCode int data any headers []headerval cookies []cookieval } func (j jsonHTTPResponse) jsonRenderer(g *gin.Context) json.GoJsonRender { var f *string if jsonfilter := g.GetString("goext.jsonfilter"); jsonfilter != "" { f = &jsonfilter } return json.GoJsonRender{Data: j.data, NilSafeSlices: true, NilSafeMaps: true, Filter: f} } func (j jsonHTTPResponse) Write(g *gin.Context) { for _, v := range j.headers { g.Header(v.Key, v.Val) } for _, v := range j.cookies { g.SetCookie(v.name, v.value, v.maxAge, v.path, v.domain, v.secure, v.httpOnly) } g.Render(j.statusCode, j.jsonRenderer(g)) } func (j jsonHTTPResponse) WithHeader(k string, v string) HTTPResponse { j.headers = append(j.headers, headerval{k, v}) return j } func (j jsonHTTPResponse) WithCookie(name string, value string, maxAge int, path string, domain string, secure bool, httpOnly bool) HTTPResponse { j.cookies = append(j.cookies, cookieval{name, value, maxAge, path, domain, secure, httpOnly}) return j } func (j jsonHTTPResponse) IsSuccess() bool { return j.statusCode >= 200 && j.statusCode <= 399 } func (j jsonHTTPResponse) Statuscode() int { return j.statusCode } func (j jsonHTTPResponse) BodyString(g *gin.Context) *string { if str, err := j.jsonRenderer(g).RenderString(); err == nil { return &str } else { return nil } } func (j jsonHTTPResponse) ContentType() string { return "application/json" } func (j jsonHTTPResponse) Headers() []string { return langext.ArrMap(j.headers, func(v headerval) string { return v.Key + "=" + v.Val }) } type emptyHTTPResponse struct { statusCode int headers []headerval cookies []cookieval } func (j emptyHTTPResponse) Write(g *gin.Context) { for _, v := range j.headers { g.Header(v.Key, v.Val) } for _, v := range j.cookies { g.SetCookie(v.name, v.value, v.maxAge, v.path, v.domain, v.secure, v.httpOnly) } g.Status(j.statusCode) } func (j emptyHTTPResponse) WithHeader(k string, v string) HTTPResponse { j.headers = append(j.headers, headerval{k, v}) return j } func (j emptyHTTPResponse) WithCookie(name string, value string, maxAge int, path string, domain string, secure bool, httpOnly bool) HTTPResponse { j.cookies = append(j.cookies, cookieval{name, value, maxAge, path, domain, secure, httpOnly}) return j } func (j emptyHTTPResponse) IsSuccess() bool { return j.statusCode >= 200 && j.statusCode <= 399 } func (j emptyHTTPResponse) Statuscode() int { return j.statusCode } func (j emptyHTTPResponse) BodyString(*gin.Context) *string { return nil } func (j emptyHTTPResponse) ContentType() string { return "" } func (j emptyHTTPResponse) Headers() []string { return langext.ArrMap(j.headers, func(v headerval) string { return v.Key + "=" + v.Val }) } type textHTTPResponse struct { statusCode int data string headers []headerval cookies []cookieval } func (j textHTTPResponse) Write(g *gin.Context) { for _, v := range j.headers { g.Header(v.Key, v.Val) } for _, v := range j.cookies { g.SetCookie(v.name, v.value, v.maxAge, v.path, v.domain, v.secure, v.httpOnly) } g.String(j.statusCode, "%s", j.data) } func (j textHTTPResponse) WithHeader(k string, v string) HTTPResponse { j.headers = append(j.headers, headerval{k, v}) return j } func (j textHTTPResponse) WithCookie(name string, value string, maxAge int, path string, domain string, secure bool, httpOnly bool) HTTPResponse { j.cookies = append(j.cookies, cookieval{name, value, maxAge, path, domain, secure, httpOnly}) return j } func (j textHTTPResponse) IsSuccess() bool { return j.statusCode >= 200 && j.statusCode <= 399 } func (j textHTTPResponse) Statuscode() int { return j.statusCode } func (j textHTTPResponse) BodyString(*gin.Context) *string { return langext.Ptr(j.data) } func (j textHTTPResponse) ContentType() string { return "text/plain" } func (j textHTTPResponse) Headers() []string { return langext.ArrMap(j.headers, func(v headerval) string { return v.Key + "=" + v.Val }) } type dataHTTPResponse struct { statusCode int data []byte contentType string headers []headerval cookies []cookieval } func (j dataHTTPResponse) Write(g *gin.Context) { for _, v := range j.headers { g.Header(v.Key, v.Val) } for _, v := range j.cookies { g.SetCookie(v.name, v.value, v.maxAge, v.path, v.domain, v.secure, v.httpOnly) } g.Data(j.statusCode, j.contentType, j.data) } func (j dataHTTPResponse) WithHeader(k string, v string) HTTPResponse { j.headers = append(j.headers, headerval{k, v}) return j } func (j dataHTTPResponse) WithCookie(name string, value string, maxAge int, path string, domain string, secure bool, httpOnly bool) HTTPResponse { j.cookies = append(j.cookies, cookieval{name, value, maxAge, path, domain, secure, httpOnly}) return j } func (j dataHTTPResponse) IsSuccess() bool { return j.statusCode >= 200 && j.statusCode <= 399 } func (j dataHTTPResponse) Statuscode() int { return j.statusCode } func (j dataHTTPResponse) BodyString(*gin.Context) *string { return langext.Ptr(string(j.data)) } func (j dataHTTPResponse) ContentType() string { return j.contentType } func (j dataHTTPResponse) Headers() []string { return langext.ArrMap(j.headers, func(v headerval) string { return v.Key + "=" + v.Val }) } type fileHTTPResponse struct { mimetype string filepath string filename *string headers []headerval cookies []cookieval } func (j fileHTTPResponse) Write(g *gin.Context) { g.Header("Content-Type", j.mimetype) // if we don't set it here gin does weird file-sniffing later... if j.filename != nil { g.Header("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", *j.filename)) } for _, v := range j.headers { g.Header(v.Key, v.Val) } for _, v := range j.cookies { g.SetCookie(v.name, v.value, v.maxAge, v.path, v.domain, v.secure, v.httpOnly) } g.File(j.filepath) } func (j fileHTTPResponse) WithHeader(k string, v string) HTTPResponse { j.headers = append(j.headers, headerval{k, v}) return j } func (j fileHTTPResponse) WithCookie(name string, value string, maxAge int, path string, domain string, secure bool, httpOnly bool) HTTPResponse { j.cookies = append(j.cookies, cookieval{name, value, maxAge, path, domain, secure, httpOnly}) return j } func (j fileHTTPResponse) IsSuccess() bool { return true } func (j fileHTTPResponse) Statuscode() int { return 200 } func (j fileHTTPResponse) BodyString(*gin.Context) *string { data, err := os.ReadFile(j.filepath) if err != nil { return nil } return langext.Ptr(string(data)) } func (j fileHTTPResponse) ContentType() string { return j.mimetype } func (j fileHTTPResponse) Headers() []string { return langext.ArrMap(j.headers, func(v headerval) string { return v.Key + "=" + v.Val }) } type downloadDataHTTPResponse struct { statusCode int mimetype string data []byte filename *string headers []headerval cookies []cookieval } func (j downloadDataHTTPResponse) Write(g *gin.Context) { g.Header("Content-Type", j.mimetype) // if we don't set it here gin does weird file-sniffing later... if j.filename != nil { g.Header("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", *j.filename)) } for _, v := range j.headers { g.Header(v.Key, v.Val) } for _, v := range j.cookies { g.SetCookie(v.name, v.value, v.maxAge, v.path, v.domain, v.secure, v.httpOnly) } g.Data(j.statusCode, j.mimetype, j.data) } func (j downloadDataHTTPResponse) WithHeader(k string, v string) HTTPResponse { j.headers = append(j.headers, headerval{k, v}) return j } func (j downloadDataHTTPResponse) WithCookie(name string, value string, maxAge int, path string, domain string, secure bool, httpOnly bool) HTTPResponse { j.cookies = append(j.cookies, cookieval{name, value, maxAge, path, domain, secure, httpOnly}) return j } func (j downloadDataHTTPResponse) IsSuccess() bool { return j.statusCode >= 200 && j.statusCode <= 399 } func (j downloadDataHTTPResponse) Statuscode() int { return j.statusCode } func (j downloadDataHTTPResponse) BodyString(*gin.Context) *string { return langext.Ptr(string(j.data)) } func (j downloadDataHTTPResponse) ContentType() string { return j.mimetype } func (j downloadDataHTTPResponse) Headers() []string { return langext.ArrMap(j.headers, func(v headerval) string { return v.Key + "=" + v.Val }) } type redirectHTTPResponse struct { statusCode int url string headers []headerval cookies []cookieval } func (j redirectHTTPResponse) Write(g *gin.Context) { for _, v := range j.headers { g.Header(v.Key, v.Val) } for _, v := range j.cookies { g.SetCookie(v.name, v.value, v.maxAge, v.path, v.domain, v.secure, v.httpOnly) } g.Redirect(j.statusCode, j.url) } func (j redirectHTTPResponse) WithHeader(k string, v string) HTTPResponse { j.headers = append(j.headers, headerval{k, v}) return j } func (j redirectHTTPResponse) WithCookie(name string, value string, maxAge int, path string, domain string, secure bool, httpOnly bool) HTTPResponse { j.cookies = append(j.cookies, cookieval{name, value, maxAge, path, domain, secure, httpOnly}) return j } func (j redirectHTTPResponse) IsSuccess() bool { return j.statusCode >= 200 && j.statusCode <= 399 } func (j redirectHTTPResponse) Statuscode() int { return j.statusCode } func (j redirectHTTPResponse) BodyString(*gin.Context) *string { return nil } func (j redirectHTTPResponse) ContentType() string { return "" } func (j redirectHTTPResponse) Headers() []string { return langext.ArrMap(j.headers, func(v headerval) string { return v.Key + "=" + v.Val }) } type jsonAPIErrResponse struct { err *exerr.ExErr headers []headerval cookies []cookieval } func (j jsonAPIErrResponse) Write(g *gin.Context) { for _, v := range j.headers { g.Header(v.Key, v.Val) } for _, v := range j.cookies { g.SetCookie(v.name, v.value, v.maxAge, v.path, v.domain, v.secure, v.httpOnly) } exerr.Get(j.err).Output(context.Background(), g) j.err.CallListener(exerr.MethodOutput) } func (j jsonAPIErrResponse) WithHeader(k string, v string) HTTPResponse { j.headers = append(j.headers, headerval{k, v}) return j } func (j jsonAPIErrResponse) WithCookie(name string, value string, maxAge int, path string, domain string, secure bool, httpOnly bool) HTTPResponse { j.cookies = append(j.cookies, cookieval{name, value, maxAge, path, domain, secure, httpOnly}) return j } func (j jsonAPIErrResponse) IsSuccess() bool { return false } func (j jsonAPIErrResponse) Statuscode() int { return langext.Coalesce(j.err.RecursiveStatuscode(), 0) } func (j jsonAPIErrResponse) BodyString(*gin.Context) *string { if str, err := j.err.ToDefaultAPIJson(); err == nil { return &str } else { return nil } } func (j jsonAPIErrResponse) ContentType() string { return "application/json" } func (j jsonAPIErrResponse) Headers() []string { return langext.ArrMap(j.headers, func(v headerval) string { return v.Key + "=" + v.Val }) } func (j jsonAPIErrResponse) Unwrap() error { return j.err } func Status(sc int) HTTPResponse { return &emptyHTTPResponse{statusCode: sc} } func JSON(sc int, data any) HTTPResponse { return &jsonHTTPResponse{statusCode: sc, data: data} } func Data(sc int, contentType string, data []byte) HTTPResponse { return &dataHTTPResponse{statusCode: sc, contentType: contentType, data: data} } func Text(sc int, data string) HTTPResponse { return &textHTTPResponse{statusCode: sc, data: data} } func File(mimetype string, filepath string) HTTPResponse { return &fileHTTPResponse{mimetype: mimetype, filepath: filepath} } func Download(mimetype string, filepath string, filename string) HTTPResponse { return &fileHTTPResponse{mimetype: mimetype, filepath: filepath, filename: &filename} } func DownloadData(status int, mimetype string, filename string, data []byte) HTTPResponse { return &downloadDataHTTPResponse{statusCode: status, mimetype: mimetype, data: data, filename: &filename} } func Redirect(sc int, newURL string) HTTPResponse { return &redirectHTTPResponse{statusCode: sc, url: newURL} } func Error(e error) HTTPResponse { return &jsonAPIErrResponse{ err: exerr.FromError(e), } } func ErrWrap(e error, errorType exerr.ErrorType, msg string) HTTPResponse { return &jsonAPIErrResponse{ err: exerr.FromError(exerr.Wrap(e, msg).WithType(errorType).Build()), } } func NotImplemented() HTTPResponse { return Error(exerr.New(exerr.TypeNotImplemented, "").Build()) }