A client.go => client.go +104 -0
@@ 0,0 1,104 @@
+package gobwebsgql
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "time"
+
+ "git.sr.ht/~emersion/gqlclient"
+ "github.com/labstack/echo/v4"
+ "golang.org/x/text/cases"
+ "golang.org/x/text/language"
+ "netlandish.com/x/gobwebs"
+ "netlandish.com/x/gobwebs/server"
+ "netlandish.com/x/gobwebs/validate"
+)
+
+type ExecuteFunc func(ctx context.Context, op *gqlclient.Operation, result interface{}) error
+
+// Execute is the default function to call a graphql query
+func Execute(ctx context.Context, op *gqlclient.Operation, result interface{}) error {
+ var (
+ client *gqlclient.Client
+ httpClient *http.Client
+ origin string
+ )
+
+ // Requires the gobwebs server.Middleware middleware in use
+ srv := server.ForContext(ctx)
+
+ if ostr, ok := srv.Config.File.Get("graphql", "api-origin"); ok {
+ origin = ostr
+ } else {
+ origin = "http://127.0.0.1:8080/query"
+ }
+
+ httpClient = &http.Client{
+ Timeout: 30 * time.Second,
+ }
+
+ client = gqlclient.New(origin, httpClient)
+ err := client.Execute(ctx, op, &result)
+ return err
+}
+
+type errorExtension struct {
+ Code int
+ Field string
+}
+
+type queryErrorExtension struct {
+ Code string `json:"code"`
+}
+
+type ParseInputErrorsFunc func(c echo.Context, graphError *gqlclient.Error, fMap gobwebs.Map) error
+
+// ParseInputErrors parse the errors list returned by the graphql api
+// and parse them into InputErrors to be displayed in the html form
+// if the error code is `ErrNotFoundCode` the handler will return 404
+func ParseInputErrors(c echo.Context, graphError *gqlclient.Error, fMap gobwebs.Map) error {
+ if len(graphError.Extensions) == 0 {
+ return graphError
+ }
+
+ inputErrs := validate.NewInputErrors()
+ var ext errorExtension
+ err := json.Unmarshal(graphError.Extensions, &ext)
+ if err != nil {
+ // Try for query error
+ // {"code":"GRAPHQL_VALIDATION_FAILED"}
+ var qext queryErrorExtension
+ err = json.Unmarshal(graphError.Extensions, &qext)
+ if err != nil {
+ return err
+ }
+ inputErrs["_global_"] = []string{fmt.Sprintf("Query error: %s", qext.Code)}
+ return inputErrs
+ }
+
+ if ext.Code == ErrNotFoundCode {
+ return echo.NotFoundHandler(c)
+ }
+
+ if ext.Code == ErrValidationGlobalCode || ext.Code == ErrRestrictedCode {
+ inputErrs["_global_"] = []string{graphError.Message}
+ return inputErrs
+ }
+
+ if ext.Code == ErrValidationCode {
+ field, ok := fMap[ext.Field]
+ if ok {
+ // We use the mapped name
+ inputErrs[field.(string)] = graphError.Message
+ } else {
+ // We capitalize the first letter to fulfill golang name convention
+ caser := cases.Title(language.English, cases.NoLower)
+ inputErrs[caser.String(ext.Field)] = graphError.Message
+ }
+ return inputErrs
+
+ }
+ return graphError
+}
M go.mod => go.mod +5 -2
@@ 3,8 3,12 @@ module netlandish.com/x/gobwebs-graphql
go 1.21
require (
+ git.sr.ht/~emersion/gqlclient v0.0.0-20230820050442-8873fe0204b9
github.com/99designs/gqlgen v0.17.49
+ github.com/alecthomas/chroma/v2 v2.14.0
github.com/labstack/echo/v4 v4.12.0
+ github.com/vektah/gqlparser/v2 v2.5.16
+ golang.org/x/text v0.16.0
netlandish.com/x/gobwebs v0.0.0-20240628133233-8fea7ee22ba2
petersanchez.com/x/carrier v0.2.1
)
@@ 18,6 22,7 @@ require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/cloudflare/circl v1.3.9 // indirect
+ github.com/dlclark/regexp2 v1.11.0 // indirect
github.com/dustin/go-humanize v1.0.0 // indirect
github.com/emersion/go-message v0.18.1 // indirect
github.com/emersion/go-pgpmail v0.2.1 // indirect
@@ 56,12 61,10 @@ require (
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.2 // indirect
github.com/vaughan0/go-ini v0.0.0-20130923145212-a98ad7ee00ec // indirect
- github.com/vektah/gqlparser/v2 v2.5.16 // indirect
golang.org/x/crypto v0.25.0 // indirect
golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa // indirect
golang.org/x/net v0.26.0 // indirect
golang.org/x/sys v0.22.0 // indirect
- golang.org/x/text v0.16.0 // indirect
golang.org/x/time v0.5.0 // indirect
google.golang.org/protobuf v1.34.1 // indirect
gopkg.in/ini.v1 v1.57.0 // indirect
M go.sum => go.sum +12 -0
@@ 31,6 31,8 @@ cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohl
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
+git.sr.ht/~emersion/gqlclient v0.0.0-20230820050442-8873fe0204b9 h1:QNwHP6WknvS7X6MEFxCpefQb1QJMqgIIt+vn/PVoMMg=
+git.sr.ht/~emersion/gqlclient v0.0.0-20230820050442-8873fe0204b9/go.mod h1:kvl/JK0Z3VRmtbBxdOJR4ydyXVouUIcFIXgv4H6rVAY=
git.sr.ht/~sircmpwn/dowork v0.0.0-20221010085743-46c4299d76a1 h1:EvPKkneKkF/f7zEgKPqIZVyj3jWO8zSmsBOvMhAGqMA=
git.sr.ht/~sircmpwn/dowork v0.0.0-20221010085743-46c4299d76a1/go.mod h1:8neHEO3503w/rNtttnR0JFpQgM/GFhaafVwvkPsFIDw=
github.com/99designs/gqlgen v0.17.49 h1:b3hNGexHd33fBSAd4NDT/c3NCcQzcAVkknhN9ym36YQ=
@@ 44,6 46,12 @@ github.com/PuerkitoBio/goquery v1.9.2 h1:4/wZksC3KgkQw7SQgkKotmKljk0M6V8TUvA8Wb4
github.com/PuerkitoBio/goquery v1.9.2/go.mod h1:GHPCaP0ODyyxqcNoFGYlAprUFH81NuRPd0GX3Zu2Mvk=
github.com/agnivade/levenshtein v1.1.1 h1:QY8M92nrzkmr798gCo3kmMyqXFzdQVpxLlGPRBij0P8=
github.com/agnivade/levenshtein v1.1.1/go.mod h1:veldBMzWxcCG2ZvUTKD2kJNRdCk5hVbJomOvKkmgYbo=
+github.com/alecthomas/assert/v2 v2.7.0 h1:QtqSACNS3tF7oasA8CU6A6sXZSBDqnm7RfpLl9bZqbE=
+github.com/alecthomas/assert/v2 v2.7.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
+github.com/alecthomas/chroma/v2 v2.14.0 h1:R3+wzpnUArGcQz7fCETQBzO5n9IMNi13iIs46aU4V9E=
+github.com/alecthomas/chroma/v2 v2.14.0/go.mod h1:QolEbTfmUHIMVpBqxeDnNBj2uoeI4EbYP4i6n68SG4I=
+github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
+github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
@@ 82,6 90,8 @@ 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/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48 h1:fRzb/w+pyskVMQ+UbP35JkH8yB7MYb4q/qhBarqZE6g=
github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA=
+github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
+github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/emersion/go-message v0.17.0/go.mod h1:/9Bazlb1jwUNB0npYYBsdJ2EMOiiyN3m5UVHbY7GoNw=
@@ 181,6 191,8 @@ github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
+github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
+github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
M graphql.go => graphql.go +19 -20
@@ 35,6 35,7 @@ type GraphQL struct {
type GQLExtension struct {
GQL *GraphQL
s *server.Server
+ eg *echo.Group
}
func (g *GQLExtension) Extend(gserver *server.Server) (*server.Server, error) {
@@ 80,35 81,32 @@ func (g *GQLExtension) Extend(gserver *server.Server) (*server.Server, error) {
srv.SetRecoverFunc(g.gqlEmailRecover)
srv.Use(extension.FixedComplexityLimit(g.GQL.MaxComplexity))
- if g.s.Config.Debug {
- g.s.Echo().GET("/graphql", func(c echo.Context) error {
- // Gross
- ctx := server.EchoContext(c.Request().Context(), c)
- c.SetRequest(c.Request().WithContext(ctx))
- phand := playground.Handler("GraphQL playground", "/query")
- phand.ServeHTTP(c.Response(), c.Request())
- return nil
- })
- }
- g.s.Echo().POST("/query", func(c echo.Context) error {
- // More gross
+ qroute := g.eg.POST("/query", func(c echo.Context) error {
+ // Hate this...
ctx := server.EchoContext(c.Request().Context(), c)
c.SetRequest(c.Request().WithContext(ctx))
srv.ServeHTTP(c.Response(), c.Request())
return nil
})
- g.s.Echo().GET("/query/api-scopes.json", func(c echo.Context) error {
+ g.eg.GET("/query/api-scopes.json", func(c echo.Context) error {
info := struct {
Scopes []string `json:"scopes"`
}{g.GQL.Scopes}
- //j, err := json.Marshal(&info)
- //if err != nil {
- // panic(err)
- //}
-
return c.JSON(http.StatusOK, &info)
})
+ if g.s.Config.Debug {
+ // If debug is set, add a handler for the default playground
+ g.eg.GET("/graphql-debug", func(c echo.Context) error {
+ // Hate this...
+ ctx := server.EchoContext(c.Request().Context(), c)
+ c.SetRequest(c.Request().WithContext(ctx))
+ phand := playground.Handler("GraphQL playground", qroute.Path)
+ phand.ServeHTTP(c.Response(), c.Request())
+ return nil
+ })
+ }
+
return g.s, nil
}
@@ 210,7 208,8 @@ The following stack trace was produced:
return fmt.Errorf("internal system error")
}
-func New(schema graphql.ExecutableSchema, scopes []string) *GQLExtension {
+// NewGQLExtension will return a GQLExtension that can be passed to a gobwebs server
+func NewGQLExtension(eg *echo.Group, schema graphql.ExecutableSchema, scopes []string) *GQLExtension {
gql := &GraphQL{Schema: schema, Scopes: scopes}
- return &GQLExtension{GQL: gql}
+ return &GQLExtension{GQL: gql, eg: eg}
}
A input.go => input.go +23 -0
@@ 0,0 1,23 @@
+package gobwebsgql
+
+import (
+ "github.com/labstack/echo/v4"
+ "netlandish.com/x/gobwebs/validate"
+)
+
+// PlaygroundForm ...
+type PlaygroundForm struct {
+ Query string `form:"query" validate:"required"`
+}
+
+// Validate ...
+func (p *PlaygroundForm) Validate(c echo.Context) error {
+ errs := validate.FormFieldBinder(c, p).
+ FailFast(false).
+ String("query", &p.Query).
+ BindErrors()
+ if errs != nil {
+ return validate.GetInputErrors(errs)
+ }
+ return c.Validate(p)
+}
A interfaces.go => interfaces.go +13 -0
@@ 0,0 1,13 @@
+package gobwebsgql
+
+import (
+ "github.com/labstack/echo/v4"
+ "netlandish.com/x/gobwebs"
+)
+
+// Helper is an interface to allow you to customize behavior
+// after a specific action from the application routes
+type Helper interface {
+ // Personal access and authorized client tokens
+ GQLPlaygroundTemplateVars(c echo.Context) gobwebs.Map
+}
A routes.go => routes.go +229 -0
@@ 0,0 1,229 @@
+package gobwebsgql
+
+import (
+ "bufio"
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "html/template"
+ "net/http"
+
+ "git.sr.ht/~emersion/gqlclient"
+ "github.com/alecthomas/chroma/v2/formatters/html"
+ "github.com/alecthomas/chroma/v2/lexers"
+ "github.com/alecthomas/chroma/v2/styles"
+ "github.com/labstack/echo/v4"
+ "netlandish.com/x/gobwebs"
+ "netlandish.com/x/gobwebs/auth"
+ "netlandish.com/x/gobwebs/core"
+ "netlandish.com/x/gobwebs/server"
+ "netlandish.com/x/gobwebs/validate"
+)
+
+// Used by the graphql playground
+var cssStyle, schemaHTML template.HTML
+
+// ServiceConfig let's you add basic config variables to service
+type ServiceConfig struct {
+ // Schema is the graphql schema, displayed on the featured playground
+ Schema string
+
+ // Query is the default query ran on the featured playground
+ Query string
+
+ // Helper is the helper integration for external apps
+ Helper Helper
+
+ // AuthRequired is a bool to determine whether or not the user is required to be
+ // authenticated in order to use the GraphQL playground. Remember to speicifcally set
+ // this to `true` if you want it to be required.
+ AuthRequired bool
+
+ // RenderFunc can be used to customize the template renderer
+ RenderFunc validate.TemplateRenderFunc
+
+ // ExecuteFunc will be used for the graphql query calls
+ ExecuteFunc ExecuteFunc
+
+ // ParseInputErrorsFunc will be used to process graphql errors for use in html forms.
+ // This is called from the GraphQL playground
+ ParseInputErrorsFunc ParseInputErrorsFunc
+}
+
+// Service is the GraphQL helper handlers struct
+type Service struct {
+ name string
+ eg *echo.Group
+
+ config *ServiceConfig
+ render validate.TemplateRenderFunc
+ execute ExecuteFunc
+ parse ParseInputErrorsFunc
+}
+
+// RegisterRoutes ...
+func (s *Service) RegisterRoutes() {
+ if s.config.AuthRequired {
+ s.eg.Use(auth.AuthRequired())
+ }
+ s.eg.GET("/graphql", s.GQLPlayground).Name = s.RouteName("gql_playground")
+ s.eg.POST("/graphql", s.GQLPlayground).Name = s.RouteName("gql_playground_post")
+}
+
+func (s *Service) GQLPlayground(c echo.Context) error {
+ result := make(map[string]any)
+
+ lt := core.GetSessionLocalizer(c)
+ pd := core.NewPageData(lt.Translate("GraphQL Playground"))
+ pd.Data["submit_query"] = lt.Translate("Submit Query")
+ pd.Data["back_home"] = lt.Translate("Back Home")
+
+ form := &PlaygroundForm{}
+ gmap := s.config.Helper.GQLPlaygroundTemplateVars(c)
+ gmap["query"] = s.config.Query
+
+ formatter := html.New(html.WithClasses(true))
+ style := styles.Get("monokailight")
+
+ if cssStyle == "" {
+ var cssBuf bytes.Buffer
+ cssw := bufio.NewWriter(&cssBuf)
+ if err := formatter.WriteCSS(cssw, style); err != nil {
+ return fmt.Errorf("Error generating schema HTML: %w", err)
+ }
+
+ cssw.Flush()
+ cssStyle = template.HTML(fmt.Sprintf("<style>%s</style>", cssBuf.String()))
+ }
+ gmap["style"] = cssStyle
+
+ if schemaHTML == "" && s.config.Schema != "" {
+ lexer := lexers.Get("graphql")
+ var schemaBuf bytes.Buffer
+
+ iterator, err := lexer.Tokenise(nil, s.config.Schema)
+ if err != nil {
+ return fmt.Errorf("Error tokenizing schema: %w", err)
+ }
+
+ schw := bufio.NewWriter(&schemaBuf)
+ err = formatter.Format(schw, style, iterator)
+ if err != nil {
+ return fmt.Errorf("Error formatting schema: %w", err)
+ }
+
+ schw.Flush()
+ schemaHTML = template.HTML(schemaBuf.String())
+ }
+ gmap["schema"] = schemaHTML
+
+ req := c.Request()
+ if req.Method == http.MethodPost {
+ if err := form.Validate(c); err != nil {
+ switch err.(type) {
+ case validate.InputErrors:
+ gmap["errors"] = err
+ gmap["form"] = form
+ return s.Render(c, http.StatusOK, "graphql.html", gmap)
+ default:
+ return err
+ }
+ }
+
+ gmap["query"] = form.Query
+ op := gqlclient.NewOperation(form.Query)
+ err := s.execute(c.Request().Context(), op, &result)
+ if err != nil {
+ if graphError, ok := err.(*gqlclient.Error); ok {
+ gmap["form"] = form
+ err = s.parse(c, graphError, gobwebs.Map{})
+ switch err.(type) {
+ case validate.InputErrors:
+ gmap["errors"] = err
+ default:
+ inputErrs := validate.NewInputErrors()
+ inputErrs["_global_"] = []string{graphError.Message}
+ gmap["errors"] = inputErrs
+ }
+ return s.Render(c, http.StatusOK, "graphql.html", gmap)
+ }
+ return err
+ }
+ } else {
+ form.Query = s.config.Query
+ if s.config.Query != "" {
+ op := gqlclient.NewOperation(s.config.Query)
+ err := s.execute(c.Request().Context(), op, &result)
+ if err != nil {
+ return err
+ }
+ }
+ gmap["form"] = form
+ }
+
+ // Pretty dumb to re-parse this but better than adjusting the existing
+ // gql request code
+ res, err := json.MarshalIndent(result, "", " ")
+ if err != nil {
+ return err
+ }
+
+ var resBuf bytes.Buffer
+ resw := bufio.NewWriter(&resBuf)
+ lexer := lexers.Get("json")
+ iterator, err := lexer.Tokenise(nil, string(res))
+ if err != nil {
+ return fmt.Errorf("Error tokenizing json response: %w", err)
+ }
+
+ err = formatter.Format(resw, style, iterator)
+ if err != nil {
+ return fmt.Errorf("Error formatting json response: %w", err)
+ }
+
+ resw.Flush()
+ gmap["results"] = template.HTML(resBuf.String())
+ return s.Render(c, http.StatusOK, "graphql.html", gmap)
+}
+
+func (s *Service) Render(c echo.Context, code int, name string, data interface{}) error {
+ if s.render != nil {
+ return s.render(c, code, name, data)
+ }
+ gctx := c.(*server.Context)
+ return gctx.Render(code, name, data)
+}
+
+// RouteName ...
+func (s *Service) RouteName(value string) string {
+ return fmt.Sprintf("%s:%s", s.name, value)
+}
+
+// NewService return service
+func NewService(eg *echo.Group, config *ServiceConfig) *Service {
+ if config.Helper == nil {
+ panic(fmt.Errorf("No gobwebsgql Helper interface provided via ServiceConfig"))
+ }
+
+ service := &Service{
+ name: "graphql",
+ eg: eg,
+ config: config,
+ execute: Execute,
+ parse: ParseInputErrors,
+ }
+ if config != nil {
+ if config.RenderFunc != nil {
+ service.render = config.RenderFunc
+ }
+ if config.ExecuteFunc != nil {
+ service.execute = config.ExecuteFunc
+ }
+ if config.ParseInputErrorsFunc != nil {
+ service.parse = config.ParseInputErrorsFunc
+ }
+ }
+
+ service.RegisterRoutes()
+ return service
+}
A valid.go => valid.go +265 -0
@@ 0,0 1,265 @@
+package gobwebsgql
+
+// This file and concept is taken from https://git.sr.ht/~sircmpwn/core-go. It's
+// modified to fit the needs of a gobwebs project but the license for this file
+// is listed below.
+
+//Copyright 2020 Drew DeVault <sir@cmpwn.com>
+
+//Redistribution and use in source and binary forms, with or without
+//modification, are permitted provided that the following conditions are met:
+
+//1. Redistributions of source code must retain the above copyright notice, this
+//list of conditions and the following disclaimer.
+
+//2. Redistributions in binary form must reproduce the above copyright notice,
+//this list of conditions and the following disclaimer in the documentation
+//and/or other materials provided with the distribution.
+
+//3. Neither the name of the copyright holder nor the names of its contributors
+//may be used to endorse or promote products derived from this software without
+//specific prior written permission.
+
+//THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+//ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+//WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+//DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+//FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+//DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+//SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+//CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+//OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+//OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+import (
+ "context"
+ "errors"
+ "fmt"
+
+ "github.com/99designs/gqlgen/graphql"
+ "github.com/vektah/gqlparser/v2/gqlerror"
+)
+
+// Standard codes used in gobwebs projects
+const (
+ ErrValidationCode int = 100
+ ErrNotFoundCode int = 200
+ ErrValidationGlobalCode int = 300
+ ErrRestrictedCode int = 400
+)
+
+var (
+ // ErrAuthorization ...
+ ErrAuthorization = errors.New("Authorizatoin required")
+)
+
+// Validation ...
+type Validation struct {
+ ctx context.Context
+ input map[string]interface{}
+}
+
+// ValidationError ...
+type ValidationError struct {
+ valid *Validation
+ err *gqlerror.Error
+}
+
+// Error Returns a new GraphQL error attached to the given field.
+func Error(ctx context.Context, field string, msg string) error {
+ return &gqlerror.Error{
+ Message: msg,
+ Path: graphql.GetPath(ctx),
+ Extensions: map[string]interface{}{
+ "field": field,
+ },
+ }
+}
+
+// Errorf Returns a new GraphQL error attached to the given field.
+func Errorf(ctx context.Context, field string, msg string, items ...interface{}) error {
+ return &gqlerror.Error{
+ Message: fmt.Sprintf(msg, items...),
+ Path: graphql.GetPath(ctx),
+ Extensions: map[string]interface{}{
+ "field": field,
+ },
+ }
+}
+
+// New Creates a new validation context.
+func New(ctx context.Context) *Validation {
+ return &Validation{
+ ctx: ctx,
+ }
+}
+
+// WithInput Adds an input map to a validation context.
+func (valid *Validation) WithInput(input map[string]interface{}) *Validation {
+ valid.input = input
+ return valid
+}
+
+// Ok Returns true if no errors were found.
+func (valid *Validation) Ok() bool {
+ return len(graphql.GetErrors(valid.ctx)) == 0
+}
+
+// Optional Fetches an item from the validation context, which must have an input
+// registered. If the field is not present, the callback is not run. Otherwise,
+// the function is called with the value for the user to conduct further
+// validation with.
+func (valid *Validation) Optional(name string, fn func(i interface{})) {
+ if valid.input == nil {
+ panic("Attempted to validate fields without input")
+ }
+ if o, ok := valid.input[name]; ok {
+ if o == nil {
+ return
+ }
+ fn(o)
+ }
+}
+
+// OptionalString Fetches a string from the validation context, which must have an input
+// registered. If the field is not present, the callback is not run. If
+// present, but not a string, an error is recorded. Otherwise, the function is
+// called with the string for the user to conduct further validation with.
+func (valid *Validation) OptionalString(name string, fn func(s string)) {
+ if valid.input == nil {
+ panic("Attempted to validate fields without input")
+ }
+ if o, ok := valid.input[name]; ok {
+ if o == nil {
+ return
+ }
+ var val string
+ switch s := o.(type) {
+ case string:
+ val = s
+ case *string:
+ val = *s
+ default:
+ valid.
+ Error("Expected %s to be a string", name).
+ WithField(name)
+ return
+ }
+ fn(val)
+ }
+}
+
+// NullableString Fetches a nullable string from the validation context, which must have an
+// input registered. If the field is not present, the callback is not run. If
+// present, but null, the function is called with null set to true. Otherwise,
+// the function is called with the string for the user to conduct further
+// validation with.
+func (valid *Validation) NullableString(name string, fn func(s *string)) {
+ if valid.input == nil {
+ panic("Attempted to validate fields without input")
+ }
+ if o, ok := valid.input[name]; ok {
+ var val *string
+ if o != nil {
+ switch s := o.(type) {
+ case string:
+ val = &s
+ case *string:
+ val = s
+ default:
+ valid.
+ Error("Expected %s to be a string", name).
+ WithField(name)
+ return
+ }
+ }
+ fn(val)
+ }
+}
+
+// OptionalBool Fetches a boolean from the validation context, which must have an input
+// registered. If the field is not present, the callback is not run. If
+// present, but not a boolean, an error is recorded. Otherwise, the function is
+// called with the boolean for the user to conduct further validation with.
+func (valid *Validation) OptionalBool(name string, fn func(b bool)) {
+ if valid.input == nil {
+ panic("Attempted to validate fields without input")
+ }
+ if o, ok := valid.input[name]; ok {
+ if o == nil {
+ return
+ }
+ var val bool
+ switch b := o.(type) {
+ case bool:
+ val = b
+ case *bool:
+ val = *b
+ default:
+ valid.
+ Error("Expected %s to be a bool", name).
+ WithField(name)
+ return
+ }
+ fn(val)
+ }
+}
+
+// Creates a validation error unconditionally.
+func (valid *Validation) Error(msg string,
+ items ...interface{}) *ValidationError {
+ err := &gqlerror.Error{
+ Path: graphql.GetPath(valid.ctx),
+ Message: fmt.Sprintf(msg, items...),
+ }
+ graphql.AddError(valid.ctx, err)
+ return &ValidationError{
+ valid: valid,
+ err: err,
+ }
+}
+
+// Expect Asserts that a condition is true, recording a GraphQL error with the given
+// message if not.
+func (valid *Validation) Expect(cond bool,
+ msg string, items ...interface{}) *ValidationError {
+ if cond {
+ return &ValidationError{valid: valid}
+ }
+ return valid.Error(msg, items...)
+}
+
+// WithField Associates a field name with an error.
+func (err *ValidationError) WithField(field string) *ValidationError {
+ if err.err == nil {
+ return err
+ }
+ if err.err.Extensions == nil {
+ err.err.Extensions = make(map[string]interface{})
+ }
+ err.err.Extensions["field"] = field
+ return err
+}
+
+// WithCode Associates an error message with an error code.
+func (err *ValidationError) WithCode(code int) *ValidationError {
+ if err.err == nil {
+ return err
+ }
+ if err.err.Extensions == nil {
+ err.err.Extensions = make(map[string]interface{})
+ }
+ err.err.Extensions["code"] = code
+ return err
+}
+
+// And Composes another assertion onto the same validation context which initially
+// created an error. Short-circuiting is used, such that if the earlier
+// condition failed, the new condition is not considered.
+func (err *ValidationError) And(cond bool,
+ msg string, items ...interface{}) *ValidationError {
+ if err.err != nil {
+ return err
+ }
+ return err.valid.Expect(cond, msg, items...)
+}