From 26c216461fa566f2ce26e3428ed317111a910347 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 29 Jul 2025 16:03:52 +0000 Subject: [PATCH] chore(deps): Bump github.com/oapi-codegen/oapi-codegen/v2 Bumps [github.com/oapi-codegen/oapi-codegen/v2](https://github.com/oapi-codegen/oapi-codegen) from 2.4.1 to 2.5.0. - [Release notes](https://github.com/oapi-codegen/oapi-codegen/releases) - [Commits](https://github.com/oapi-codegen/oapi-codegen/compare/v2.4.1...v2.5.0) --- updated-dependencies: - dependency-name: github.com/oapi-codegen/oapi-codegen/v2 dependency-version: 2.5.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- go.mod | 7 +- go.sum | 17 +- .../v2/cmd/oapi-codegen/oapi-codegen.go | 18 +- .../oapi-codegen/v2/pkg/codegen/codegen.go | 26 +- .../v2/pkg/codegen/configuration.go | 67 + .../oapi-codegen/v2/pkg/codegen/extension.go | 9 + .../v2/pkg/codegen/minimum_go_version.go | 91 + .../oapi-codegen/v2/pkg/codegen/operations.go | 39 +- .../oapi-codegen/v2/pkg/codegen/schema.go | 144 +- .../v2/pkg/codegen/server_urls.go | 81 + .../v2/pkg/codegen/template_helpers.go | 23 + .../templates/additional-properties.tmpl | 6 +- .../codegen/templates/chi/chi-middleware.tmpl | 14 +- .../templates/client-with-responses.tmpl | 7 + .../v2/pkg/codegen/templates/client.tmpl | 30 +- .../codegen/templates/echo/echo-wrappers.tmpl | 14 +- .../templates/fiber/fiber-middleware.tmpl | 14 +- .../codegen/templates/gin/gin-wrappers.tmpl | 14 +- .../templates/gorilla/gorilla-middleware.tmpl | 14 +- .../templates/iris/iris-middleware.tmpl | 14 +- .../v2/pkg/codegen/templates/server-urls.tmpl | 61 + .../stdhttp/std-http-middleware.tmpl | 14 +- .../union-and-additional-properties.tmpl | 4 +- .../v2/pkg/codegen/templates/union.tmpl | 4 +- .../oapi-codegen/v2/pkg/codegen/utils.go | 33 +- .../oapi-codegen/v2/pkg/util/loader.go | 18 +- .../github.com/speakeasy-api/jsonpath/LICENSE | 201 ++ .../jsonpath/pkg/jsonpath/config/config.go | 31 + .../jsonpath/pkg/jsonpath/filter.go | 456 ++++ .../jsonpath/pkg/jsonpath/jsonpath.go | 35 + .../jsonpath/pkg/jsonpath/parser.go | 729 +++++++ .../jsonpath/pkg/jsonpath/segment.go | 83 + .../jsonpath/pkg/jsonpath/selector.go | 63 + .../jsonpath/pkg/jsonpath/token/token.go | 776 +++++++ .../jsonpath/pkg/jsonpath/yaml_eval.go | 278 +++ .../jsonpath/pkg/jsonpath/yaml_query.go | 393 ++++ .../openapi-overlay/pkg/overlay/apply.go | 110 +- .../openapi-overlay/pkg/overlay/compare.go | 3 +- .../openapi-overlay/pkg/overlay/jsonpath.go | 47 + .../openapi-overlay/pkg/overlay/schema.go | 6 +- vendor/golang.org/x/mod/modfile/print.go | 184 ++ vendor/golang.org/x/mod/modfile/read.go | 964 +++++++++ vendor/golang.org/x/mod/modfile/rule.go | 1836 +++++++++++++++++ vendor/golang.org/x/mod/modfile/work.go | 335 +++ vendor/modules.txt | 16 +- 45 files changed, 7119 insertions(+), 210 deletions(-) create mode 100644 vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/minimum_go_version.go create mode 100644 vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/server_urls.go create mode 100644 vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/templates/server-urls.tmpl create mode 100644 vendor/github.com/speakeasy-api/jsonpath/LICENSE create mode 100644 vendor/github.com/speakeasy-api/jsonpath/pkg/jsonpath/config/config.go create mode 100644 vendor/github.com/speakeasy-api/jsonpath/pkg/jsonpath/filter.go create mode 100644 vendor/github.com/speakeasy-api/jsonpath/pkg/jsonpath/jsonpath.go create mode 100644 vendor/github.com/speakeasy-api/jsonpath/pkg/jsonpath/parser.go create mode 100644 vendor/github.com/speakeasy-api/jsonpath/pkg/jsonpath/segment.go create mode 100644 vendor/github.com/speakeasy-api/jsonpath/pkg/jsonpath/selector.go create mode 100644 vendor/github.com/speakeasy-api/jsonpath/pkg/jsonpath/token/token.go create mode 100644 vendor/github.com/speakeasy-api/jsonpath/pkg/jsonpath/yaml_eval.go create mode 100644 vendor/github.com/speakeasy-api/jsonpath/pkg/jsonpath/yaml_query.go create mode 100644 vendor/github.com/speakeasy-api/openapi-overlay/pkg/overlay/jsonpath.go create mode 100644 vendor/golang.org/x/mod/modfile/print.go create mode 100644 vendor/golang.org/x/mod/modfile/read.go create mode 100644 vendor/golang.org/x/mod/modfile/rule.go create mode 100644 vendor/golang.org/x/mod/modfile/work.go diff --git a/go.mod b/go.mod index 2e9959b..9539d9d 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.24 require ( github.com/jacobbrewer1/uhttp v0.0.12 github.com/jmoiron/sqlx v1.4.0 - github.com/oapi-codegen/oapi-codegen/v2 v2.4.1 + github.com/oapi-codegen/oapi-codegen/v2 v2.5.0 github.com/oapi-codegen/runtime v1.1.2 github.com/stretchr/testify v1.10.0 github.com/vektra/mockery/v2 v2.53.4 @@ -18,7 +18,7 @@ require ( github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936 // indirect github.com/fsnotify/fsnotify v1.8.0 // indirect - github.com/getkin/kin-openapi v0.131.0 // indirect + github.com/getkin/kin-openapi v0.132.0 // indirect github.com/go-openapi/jsonpointer v0.21.0 // indirect github.com/go-openapi/swag v0.23.0 // indirect github.com/go-viper/mapstructure/v2 v2.2.1 // indirect @@ -49,7 +49,8 @@ require ( github.com/rs/zerolog v1.33.0 // indirect github.com/sagikazarmark/locafero v0.7.0 // indirect github.com/sourcegraph/conc v0.3.0 // indirect - github.com/speakeasy-api/openapi-overlay v0.9.0 // indirect + github.com/speakeasy-api/jsonpath v0.6.0 // indirect + github.com/speakeasy-api/openapi-overlay v0.10.2 // indirect github.com/spf13/afero v1.12.0 // indirect github.com/spf13/cast v1.7.1 // indirect github.com/spf13/cobra v1.8.1 // indirect diff --git a/go.sum b/go.sum index 1c59cc9..acd145b 100644 --- a/go.sum +++ b/go.sum @@ -24,8 +24,8 @@ github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMo github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= -github.com/getkin/kin-openapi v0.131.0 h1:NO2UeHnFKRYhZ8wg6Nyh5Cq7dHk4suQQr72a4pMrDxE= -github.com/getkin/kin-openapi v0.131.0/go.mod h1:3OlG51PCYNsPByuiMB0t4fjnNlIDnaEDsjiKUV8nL58= +github.com/getkin/kin-openapi v0.132.0 h1:3ISeLMsQzcb5v26yeJrBcdTCEQTag36ZjaGk7MIRUwk= +github.com/getkin/kin-openapi v0.132.0/go.mod h1:3OlG51PCYNsPByuiMB0t4fjnNlIDnaEDsjiKUV8nL58= github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= @@ -109,8 +109,8 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8m github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= -github.com/oapi-codegen/oapi-codegen/v2 v2.4.1 h1:ykgG34472DWey7TSjd8vIfNykXgjOgYJZoQbKfEeY/Q= -github.com/oapi-codegen/oapi-codegen/v2 v2.4.1/go.mod h1:N5+lY1tiTDV3V1BeHtOxeWXHoPVeApvsvjJqegfoaz8= +github.com/oapi-codegen/oapi-codegen/v2 v2.5.0 h1:iJvF8SdB/3/+eGOXEpsWkD8FQAHj6mqkb6Fnsoc8MFU= +github.com/oapi-codegen/oapi-codegen/v2 v2.5.0/go.mod h1:fwlMxUEMuQK5ih9aymrxKPQqNm2n8bdLk1ppjH+lr9w= github.com/oapi-codegen/runtime v1.1.2 h1:P2+CubHq8fO4Q6fV1tqDBZHCwpVpvPg7oKiYzQgXIyI= github.com/oapi-codegen/runtime v1.1.2/go.mod h1:SK9X900oXmPWilYR5/WKPzt3Kqxn/uS/+lbpREv+eCg= github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 h1:G7ERwszslrBzRxj//JalHPu/3yz+De2J+4aLtSRlHiY= @@ -127,9 +127,8 @@ github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1Cpa github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= +github.com/onsi/gomega v1.19.0 h1:4ieX6qQjPP/BfC3mpsAtIGGlxTWPeA3Inl/7DtXw1tw= github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= -github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE= -github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg= github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s= @@ -158,8 +157,10 @@ github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= -github.com/speakeasy-api/openapi-overlay v0.9.0 h1:Wrz6NO02cNlLzx1fB093lBlYxSI54VRhy1aSutx0PQg= -github.com/speakeasy-api/openapi-overlay v0.9.0/go.mod h1:f5FloQrHA7MsxYg9djzMD5h6dxrHjVVByWKh7an8TRc= +github.com/speakeasy-api/jsonpath v0.6.0 h1:IhtFOV9EbXplhyRqsVhHoBmmYjblIRh5D1/g8DHMXJ8= +github.com/speakeasy-api/jsonpath v0.6.0/go.mod h1:ymb2iSkyOycmzKwbEAYPJV/yi2rSmvBCLZJcyD+VVWw= +github.com/speakeasy-api/openapi-overlay v0.10.2 h1:VOdQ03eGKeiHnpb1boZCGm7x8Haj6gST0P3SGTX95GU= +github.com/speakeasy-api/openapi-overlay v0.10.2/go.mod h1:n0iOU7AqKpNFfEt6tq7qYITC4f0yzVVdFw0S7hukemg= github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs= github.com/spf13/afero v1.12.0/go.mod h1:ZTlWwG4/ahT8W7T0WQ5uYmjI9duaLQGy3Q2OAl4sk/4= github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y= diff --git a/vendor/github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen/oapi-codegen.go b/vendor/github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen/oapi-codegen.go index 2d05cff..78bea5b 100644 --- a/vendor/github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen/oapi-codegen.go +++ b/vendor/github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen/oapi-codegen.go @@ -111,7 +111,7 @@ func main() { flag.StringVar(&flagImportMapping, "import-mapping", "", "A dict from the external reference to golang package path.") flag.StringVar(&flagExcludeSchemas, "exclude-schemas", "", "A comma separated list of schemas which must be excluded from generation.") flag.StringVar(&flagResponseTypeSuffix, "response-type-suffix", "", "The suffix used for responses types.") - flag.BoolVar(&flagAliasTypes, "alias-types", false, "Alias type declarations of possible.") + flag.BoolVar(&flagAliasTypes, "alias-types", false, "Alias type declarations if possible.") flag.BoolVar(&flagInitialismOverrides, "initialism-overrides", false, "Use initialism overrides.") flag.Parse() @@ -271,6 +271,15 @@ func main() { errExit("configuration error: %v\n", err) } + if warnings := opts.Generate.Warnings(); len(warnings) > 0 { + out := "WARNING: A number of warning(s) were returned when validating the GenerateOptions:" + for k, v := range warnings { + out += "\n- " + k + ": " + v + } + + _, _ = fmt.Fprint(os.Stderr, out) + } + // If the user asked to output configuration, output it to stdout and exit if flagOutputConfig { buf, err := yaml.Marshal(opts) @@ -297,11 +306,11 @@ func main() { } if strings.HasPrefix(swagger.OpenAPI, "3.1.") { - fmt.Println("WARNING: You are using an OpenAPI 3.1.x specification, which is not yet supported by oapi-codegen (https://github.com/oapi-codegen/oapi-codegen/issues/373) and so some functionality may not be available. Until oapi-codegen supports OpenAPI 3.1, it is recommended to downgrade your spec to 3.0.x") + fmt.Fprintln(os.Stderr, "WARNING: You are using an OpenAPI 3.1.x specification, which is not yet supported by oapi-codegen (https://github.com/oapi-codegen/oapi-codegen/issues/373) and so some functionality may not be available. Until oapi-codegen supports OpenAPI 3.1, it is recommended to downgrade your spec to 3.0.x") } if len(noVCSVersionOverride) > 0 { - opts.Configuration.NoVCSVersionOverride = &noVCSVersionOverride + opts.NoVCSVersionOverride = &noVCSVersionOverride } code, err := codegen.Generate(swagger, opts.Configuration) @@ -310,6 +319,9 @@ func main() { } if opts.OutputFile != "" { + if err := os.MkdirAll(filepath.Dir(opts.OutputFile), 0o755); err != nil { + errExit("error unable to create directory: %s\n", err) + } err = os.WriteFile(opts.OutputFile, []byte(code), 0o644) if err != nil { errExit("error writing generated code to file: %s\n", err) diff --git a/vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/codegen.go b/vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/codegen.go index 632f649..6e602ea 100644 --- a/vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/codegen.go +++ b/vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/codegen.go @@ -47,6 +47,9 @@ var globalState struct { options Configuration spec *openapi3.T importMapping importMap + // initialismsMap stores initialisms as "lower(initialism) -> initialism" map. + // List of initialisms was taken from https://staticcheck.io/docs/configuration/options/#initialisms. + initialismsMap map[string]string } // goImport represents a go package to be imported in the generated code @@ -139,6 +142,12 @@ func Generate(spec *openapi3.T, opts Configuration) (string, error) { opts.OutputOptions.NameNormalizer, NameNormalizers.Options()) } + if nameNormalizerFunction != NameNormalizerFunctionToCamelCaseWithInitialisms && len(opts.OutputOptions.AdditionalInitialisms) > 0 { + return "", fmt.Errorf("you have specified `additional-initialisms`, but the `name-normalizer` is not set to `ToCamelCaseWithInitialisms`. Please specify `name-normalizer: ToCamelCaseWithInitialisms` or remove the `additional-initialisms` configuration") + } + + globalState.initialismsMap = makeInitialismsMap(opts.OutputOptions.AdditionalInitialisms) + // This creates the golang templates text package TemplateFunctions["opts"] = func() Configuration { return globalState.options } t := template.New("oapi-codegen").Funcs(TemplateFunctions) @@ -193,6 +202,14 @@ func Generate(spec *openapi3.T, opts Configuration) (string, error) { MergeImports(xGoTypeImports, imprts) } + var serverURLsDefinitions string + if opts.Generate.ServerURLs { + serverURLsDefinitions, err = GenerateServerURLs(t, spec) + if err != nil { + return "", fmt.Errorf("error generating Server URLs: %w", err) + } + } + var irisServerOut string if opts.Generate.IrisServer { irisServerOut, err = GenerateIrisServer(t, ops) @@ -317,6 +334,11 @@ func Generate(spec *openapi3.T, opts Configuration) (string, error) { return "", fmt.Errorf("error writing constants: %w", err) } + _, err = w.WriteString(serverURLsDefinitions) + if err != nil { + return "", fmt.Errorf("error writing Server URLs: %w", err) + } + _, err = w.WriteString(typeDefinitions) if err != nil { return "", fmt.Errorf("error writing type definitions: %w", err) @@ -955,7 +977,9 @@ func GetUserTemplateText(inputData string) (template string, err error) { return "", fmt.Errorf("failed to execute GET request data from %s: %w", inputData, err) } if resp != nil { - defer resp.Body.Close() + defer func() { + _ = resp.Body.Close() + }() } if resp.StatusCode < 200 || resp.StatusCode >= 300 { return "", fmt.Errorf("got non %d status code on GET %s", resp.StatusCode, inputData) diff --git a/vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/configuration.go b/vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/configuration.go index c2d36f9..1d9ff3e 100644 --- a/vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/configuration.go +++ b/vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/configuration.go @@ -126,12 +126,43 @@ type GenerateOptions struct { Models bool `yaml:"models,omitempty"` // EmbeddedSpec indicates whether to embed the swagger spec in the generated code EmbeddedSpec bool `yaml:"embedded-spec,omitempty"` + // ServerURLs generates types for the `Server` definitions' URLs, instead of needing to provide your own values + ServerURLs bool `yaml:"server-urls,omitempty"` } func (oo GenerateOptions) Validate() map[string]string { return nil } +func (oo GenerateOptions) Warnings() map[string]string { + warnings := make(map[string]string) + + if oo.StdHTTPServer { + if warning := oo.warningForStdHTTP(); warning != "" { + warnings["std-http-server"] = warning + } + } + + return warnings +} + +func (oo GenerateOptions) warningForStdHTTP() string { + pathToGoMod, mod, err := findAndParseGoModuleForDepth(".", maximumDepthToSearchForGoMod) + if err != nil { + return fmt.Sprintf("Encountered an error while trying to find a `go.mod` or a `tools.mod` in this directory, or %d levels above it: %v", maximumDepthToSearchForGoMod, err) + } + + if mod == nil { + return fmt.Sprintf("Failed to find a `go.mod` or a `tools.mod` in this directory, or %d levels above it, so unable to validate that you're using Go 1.22+. If you start seeing API interactions resulting in a `404 page not found`, the Go directive (implying source compatibility for this module) needs to be bumped. See also: https://www.jvt.me/posts/2024/03/04/go-net-http-why-404/", maximumDepthToSearchForGoMod) + } + + if !hasMinimalMinorGoDirective(minimumGoVersionForGenerateStdHTTPServer, mod) { + return fmt.Sprintf("Found a `go.mod` or a `tools.mod` at path %v, but it only had a version of %v, whereas the minimum required is 1.%d. It's very likely API interactions will result in a `404 page not found`. The Go directive (implying source compatibility for this module) needs to be bumped. See also: https://www.jvt.me/posts/2024/03/04/go-net-http-why-404/", pathToGoMod, mod.Go.Version, minimumGoVersionForGenerateStdHTTPServer) + } + + return "" +} + // CompatibilityOptions specifies backward compatibility settings for the // code generator. type CompatibilityOptions struct { @@ -195,6 +226,14 @@ type CompatibilityOptions struct { // // NOTE that this can be confusing to users of your OpenAPI specification, who may see a field present and therefore be expecting to see/use it in the request/response, without understanding the nuance of how `oapi-codegen` generates the code. AllowUnexportedStructFieldNames bool `yaml:"allow-unexported-struct-field-names"` + + // PreserveOriginalOperationIdCasingInEmbeddedSpec ensures that the `operationId` from the source spec is kept intact in case when embedding it into the Embedded Spec output. + // When `oapi-codegen` parses the original OpenAPI specification, it will apply the configured `output-options.name-normalizer` to each operation's `operationId` before that is used to generate code from. + // However, this is also applied to the copy of the `operationId`s in the `embedded-spec` generation, which means that the embedded OpenAPI specification is then out-of-sync with the input specificiation. + // To ensure that the `operationId` in the embedded spec is preserved as-is from the input specification, set this. + // NOTE that this will not impact generated code. + // NOTE that if you're using `include-operation-ids` or `exclude-operation-ids` you may want to ensure that the `operationId`s used are correct. + PreserveOriginalOperationIdCasingInEmbeddedSpec bool `yaml:"preserve-original-operation-id-casing-in-embedded-spec"` } func (co CompatibilityOptions) Validate() map[string]string { @@ -226,6 +265,9 @@ type OutputOptions struct { ClientTypeName string `yaml:"client-type-name,omitempty"` // Whether to use the initialism overrides InitialismOverrides bool `yaml:"initialism-overrides,omitempty"` + // AdditionalInitialisms is a list of additional initialisms to use when generating names. + // NOTE that this has no effect unless the `name-normalizer` is set to `ToCamelCaseWithInitialisms` + AdditionalInitialisms []string `yaml:"additional-initialisms,omitempty"` // Whether to generate nullable type for nullable fields NullableType bool `yaml:"nullable-type,omitempty"` @@ -239,9 +281,34 @@ type OutputOptions struct { // Overlay defines configuration for the OpenAPI Overlay (https://github.com/OAI/Overlay-Specification) to manipulate the OpenAPI specification before generation. This allows modifying the specification without needing to apply changes directly to it, making it easier to keep it up-to-date. Overlay OutputOptionsOverlay `yaml:"overlay"` + + // EnableYamlTags adds YAML tags to generated structs, in addition to default JSON ones + EnableYamlTags bool `yaml:"yaml-tags,omitempty"` + + // ClientResponseBytesFunction decides whether to enable the generation of a `Bytes()` method on response objects for `ClientWithResponses` + ClientResponseBytesFunction bool `yaml:"client-response-bytes-function,omitempty"` + + // PreferSkipOptionalPointer allows defining at a global level whether to omit the pointer for a type to indicate that the field/type is optional. + // This is the same as adding `x-go-type-skip-optional-pointer` to each field (manually, or using an OpenAPI Overlay) + PreferSkipOptionalPointer bool `yaml:"prefer-skip-optional-pointer,omitempty"` + + // PreferSkipOptionalPointerWithOmitzero allows generating the `omitzero` JSON tag types that would have had an optional pointer. + // This is the same as adding `x-omitzero` to each field (manually, or using an OpenAPI Overlay). + // A field can set `x-omitzero: false` to disable the `omitzero` JSON tag. + // NOTE that this must be used alongside `prefer-skip-optional-pointer`, otherwise makes no difference. + PreferSkipOptionalPointerWithOmitzero bool `yaml:"prefer-skip-optional-pointer-with-omitzero,omitempty"` + + // PreferSkipOptionalPointerOnContainerTypes allows disabling the generation of an "optional pointer" for an optional field that is a container type (such as a slice or a map), which ends up requiring an additional, unnecessary, `... != nil` check + PreferSkipOptionalPointerOnContainerTypes bool `yaml:"prefer-skip-optional-pointer-on-container-types,omitempty"` } func (oo OutputOptions) Validate() map[string]string { + if NameNormalizerFunction(oo.NameNormalizer) != NameNormalizerFunctionToCamelCaseWithInitialisms && len(oo.AdditionalInitialisms) > 0 { + return map[string]string{ + "additional-initialisms": "You have specified `additional-initialisms`, but the `name-normalizer` is not set to `ToCamelCaseWithInitialisms`. Please specify `name-normalizer: ToCamelCaseWithInitialisms` or remove the `additional-initialisms` configuration", + } + } + return nil } diff --git a/vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/extension.go b/vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/extension.go index f5ef5ef..579d8a1 100644 --- a/vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/extension.go +++ b/vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/extension.go @@ -18,6 +18,7 @@ const ( extGoTypeName = "x-go-type-name" extPropGoJsonIgnore = "x-go-json-ignore" extPropOmitEmpty = "x-omitempty" + extPropOmitZero = "x-omitzero" extPropExtraTags = "x-oapi-codegen-extra-tags" extEnumVarNames = "x-enum-varnames" extEnumNames = "x-enumNames" @@ -60,6 +61,14 @@ func extParseOmitEmpty(extPropValue interface{}) (bool, error) { return omitEmpty, nil } +func extParseOmitZero(extPropValue interface{}) (bool, error) { + omitZero, ok := extPropValue.(bool) + if !ok { + return false, fmt.Errorf("failed to convert type: %T", extPropValue) + } + return omitZero, nil +} + func extExtraTags(extPropValue interface{}) (map[string]string, error) { tagsI, ok := extPropValue.(map[string]interface{}) if !ok { diff --git a/vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/minimum_go_version.go b/vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/minimum_go_version.go new file mode 100644 index 0000000..4f4f70f --- /dev/null +++ b/vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/minimum_go_version.go @@ -0,0 +1,91 @@ +package codegen + +import ( + "fmt" + "os" + "path/filepath" + "strconv" + "strings" + + "golang.org/x/mod/modfile" +) + +const maximumDepthToSearchForGoMod = 5 + +// minimumGoVersionForGenerateStdHTTPServer indicates the Go 1.x minor version that the module the std-http-server is being generated into needs. +// If the version is lower, a warning should be logged. +const minimumGoVersionForGenerateStdHTTPServer = 22 + +func findAndParseGoModuleForDepth(dir string, maxDepth int) (string, *modfile.File, error) { + absDir, err := filepath.Abs(dir) + if err != nil { + return "", nil, fmt.Errorf("failed to determine absolute path for %v: %w", dir, err) + } + currentDir := absDir + + for i := 0; i <= maxDepth; i++ { + goModPath := filepath.Join(currentDir, "go.mod") + if _, err := os.Stat(goModPath); err == nil { + goModContent, err := os.ReadFile(goModPath) + if err != nil { + return "", nil, fmt.Errorf("failed to read `go.mod`: %w", err) + } + + mod, err := modfile.ParseLax("go.mod", goModContent, nil) + if err != nil { + return "", nil, fmt.Errorf("failed to parse `go.mod`: %w", err) + } + + return goModPath, mod, nil + } + + goModPath = filepath.Join(currentDir, "tools.mod") + if _, err := os.Stat(goModPath); err == nil { + goModContent, err := os.ReadFile(goModPath) + if err != nil { + return "", nil, fmt.Errorf("failed to read `tools.mod`: %w", err) + } + + parsedModFile, err := modfile.ParseLax("tools.mod", goModContent, nil) + if err != nil { + return "", nil, fmt.Errorf("failed to parse `tools.mod`: %w", err) + } + + return goModPath, parsedModFile, nil + } + + parentDir := filepath.Dir(currentDir) + // NOTE that this may not work particularly well on Windows + if parentDir == "/" { + break + } + + currentDir = parentDir + } + + return "", nil, fmt.Errorf("no `go.mod` or `tools.mod` file found within %d levels upwards from %s", maxDepth, absDir) +} + +// hasMinimalMinorGoDirective indicates that the Go module (`mod`) has a minor version greater than or equal to the `expected`'s +// This only applies to the `go` directive: +// +// go 1.23 +// go 1.22.1 +func hasMinimalMinorGoDirective(expected int, mod *modfile.File) bool { + parts := strings.Split(mod.Go.Version, ".") + + if len(parts) < 2 { + return false + } + + actual, err := strconv.Atoi(parts[1]) + if err != nil { + return false + } + + if actual < expected { + return false + } + + return true +} diff --git a/vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/operations.go b/vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/operations.go index 3b0f130..e4d9784 100644 --- a/vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/operations.go +++ b/vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/operations.go @@ -135,10 +135,17 @@ func (pd ParameterDefinition) GoName() string { return SchemaNameToTypeName(goName) } +// Deprecated: Use HasOptionalPointer, as it is clearer what the intent is. func (pd ParameterDefinition) IndirectOptional() bool { return !pd.Required && !pd.Schema.SkipOptionalPointer } +// HasOptionalPointer indicates whether the generated property has an optional pointer associated with it. +// This takes into account the `x-go-type-skip-optional-pointer` extension, allowing a parameter definition to control whether the pointer should be skipped. +func (pd ParameterDefinition) HasOptionalPointer() bool { + return pd.Required == false && pd.Schema.SkipOptionalPointer == false //nolint:staticcheck +} + type ParameterDefinitions []ParameterDefinition func (p ParameterDefinitions) FindByName(name string) *ParameterDefinition { @@ -208,7 +215,8 @@ func DescribeSecurityDefinition(securityRequirements openapi3.SecurityRequiremen // OperationDefinition describes an Operation type OperationDefinition struct { - OperationId string // The operation_id description from Swagger, used to generate function names + // OperationId is the `operationId` field from the OpenAPI Specification, after going through a `nameNormalizer`, and will be used to generate function names + OperationId string PathParams []ParameterDefinition // Parameters in the path, eg, /path/:param HeaderParams []ParameterDefinition // Parameters in HTTP headers @@ -299,7 +307,7 @@ func (o *OperationDefinition) GetResponseTypeDefinitions() ([]ResponseTypeDefini if contentType.Schema != nil { responseSchema, err := GenerateGoSchema(contentType.Schema, []string{o.OperationId, responseName}) if err != nil { - return nil, fmt.Errorf("Unable to determine Go type for %s.%s: %w", o.OperationId, contentTypeName, err) + return nil, fmt.Errorf("unable to determine Go type for %s.%s: %w", o.OperationId, contentTypeName, err) } var typeName string @@ -308,7 +316,7 @@ func (o *OperationDefinition) GetResponseTypeDefinitions() ([]ResponseTypeDefini // HAL+JSON: case StringInArray(contentTypeName, contentTypesHalJSON): typeName = fmt.Sprintf("HALJSON%s", nameNormalizer(responseName)) - case "application/json" == contentTypeName: + case contentTypeName == "application/json": // if it's the standard application/json typeName = fmt.Sprintf("JSON%s", nameNormalizer(responseName)) // Vendored JSON @@ -558,25 +566,34 @@ func OperationDefinitions(swagger *openapi3.T, initialismOverrides bool) ([]Oper // Each path can have a number of operations, POST, GET, OPTIONS, etc. pathOps := pathItem.Operations() for _, opName := range SortedMapKeys(pathOps) { + // NOTE that this is a reference to the existing copy of the Operation, so any modifications will modify our shared copy of the spec op := pathOps[opName] + if pathItem.Servers != nil { op.Servers = &pathItem.Servers } + // take a copy of operationId, so we don't modify the underlying spec + operationId := op.OperationID // We rely on OperationID to generate function names, it's required - if op.OperationID == "" { - op.OperationID, err = generateDefaultOperationID(opName, requestPath, toCamelCaseFunc) + if operationId == "" { + operationId, err = generateDefaultOperationID(opName, requestPath, toCamelCaseFunc) if err != nil { return nil, fmt.Errorf("error generating default OperationID for %s/%s: %s", opName, requestPath, err) } } else { - op.OperationID = nameNormalizer(op.OperationID) + operationId = nameNormalizer(operationId) + } + operationId = typeNamePrefix(operationId) + operationId + + if !globalState.options.Compatibility.PreserveOriginalOperationIdCasingInEmbeddedSpec { + // update the existing, shared, copy of the spec if we're not wanting to preserve it + op.OperationID = operationId } - op.OperationID = typeNamePrefix(op.OperationID) + op.OperationID // These are parameters defined for the specific path method that // we're iterating over. - localParams, err := DescribeParameters(op.Parameters, []string{op.OperationID + "Params"}) + localParams, err := DescribeParameters(op.Parameters, []string{operationId + "Params"}) if err != nil { return nil, fmt.Errorf("error describing global parameters for %s/%s: %s", opName, requestPath, err) @@ -599,14 +616,14 @@ func OperationDefinitions(swagger *openapi3.T, initialismOverrides bool) ([]Oper return nil, err } - bodyDefinitions, typeDefinitions, err := GenerateBodyDefinitions(op.OperationID, op.RequestBody) + bodyDefinitions, typeDefinitions, err := GenerateBodyDefinitions(operationId, op.RequestBody) if err != nil { return nil, fmt.Errorf("error generating body definitions: %w", err) } ensureExternalRefsInRequestBodyDefinitions(&bodyDefinitions, pathItem.Ref) - responseDefinitions, err := GenerateResponseDefinitions(op.OperationID, op.Responses.Map()) + responseDefinitions, err := GenerateResponseDefinitions(operationId, op.Responses.Map()) if err != nil { return nil, fmt.Errorf("error generating response definitions: %w", err) } @@ -618,7 +635,7 @@ func OperationDefinitions(swagger *openapi3.T, initialismOverrides bool) ([]Oper HeaderParams: FilterParameterDefinitionByType(allParams, "header"), QueryParams: FilterParameterDefinitionByType(allParams, "query"), CookieParams: FilterParameterDefinitionByType(allParams, "cookie"), - OperationId: nameNormalizer(op.OperationID), + OperationId: nameNormalizer(operationId), // Replace newlines in summary. Summary: op.Summary, Method: opName, diff --git a/vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/schema.go b/vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/schema.go index 2cef002..4f983d1 100644 --- a/vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/schema.go +++ b/vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/schema.go @@ -128,6 +128,12 @@ func (p Property) GoTypeDef() string { return typeDef } +// HasOptionalPointer indicates whether the generated property has an optional pointer associated with it. +// This takes into account the `x-go-type-skip-optional-pointer` extension, allowing a parameter definition to control whether the pointer should be skipped. +func (p Property) HasOptionalPointer() bool { + return p.Required == false && p.Schema.SkipOptionalPointer == false //nolint:staticcheck +} + // EnumDefinition holds type information for enum type EnumDefinition struct { // Schema is the scheme of a type which has a list of enum values, eg, the @@ -264,16 +270,19 @@ func GenerateGoSchema(sref *openapi3.SchemaRef, path []string) (Schema, error) { sref.Ref, err) } return Schema{ - GoType: refType, - Description: schema.Description, - DefineViaAlias: true, - OAPISchema: schema, + GoType: refType, + Description: schema.Description, + DefineViaAlias: true, + OAPISchema: schema, + SkipOptionalPointer: globalState.options.OutputOptions.PreferSkipOptionalPointer, }, nil } outSchema := Schema{ Description: schema.Description, OAPISchema: schema, + // NOTE that SkipOptionalPointer will be defaulted to the global value, but can be overridden on a per-type/-field basis + SkipOptionalPointer: globalState.options.OutputOptions.PreferSkipOptionalPointer, } // AllOf is interesting, and useful. It's the union of a number of other @@ -289,6 +298,16 @@ func GenerateGoSchema(sref *openapi3.SchemaRef, path []string) (Schema, error) { return mergedSchema, nil } + // Check x-go-type-skip-optional-pointer, which will override if the type + // should be a pointer or not when the field is optional. + if extension, ok := schema.Extensions[extPropGoTypeSkipOptionalPointer]; ok { + skipOptionalPointer, err := extParsePropGoTypeSkipOptionalPointer(extension) + if err != nil { + return outSchema, fmt.Errorf("invalid value for %q: %w", extPropGoTypeSkipOptionalPointer, err) + } + outSchema.SkipOptionalPointer = skipOptionalPointer + } + // Check x-go-type, which will completely override the definition of this // schema with the provided type. if extension, ok := schema.Extensions[extPropGoType]; ok { @@ -302,16 +321,6 @@ func GenerateGoSchema(sref *openapi3.SchemaRef, path []string) (Schema, error) { return outSchema, nil } - // Check x-go-type-skip-optional-pointer, which will override if the type - // should be a pointer or not when the field is optional. - if extension, ok := schema.Extensions[extPropGoTypeSkipOptionalPointer]; ok { - skipOptionalPointer, err := extParsePropGoTypeSkipOptionalPointer(extension) - if err != nil { - return outSchema, fmt.Errorf("invalid value for %q: %w", extPropGoTypeSkipOptionalPointer, err) - } - outSchema.SkipOptionalPointer = skipOptionalPointer - } - // Schema type and format, eg. string / binary t := schema.Type // Handle objects and empty schemas first as a special case @@ -325,10 +334,13 @@ func GenerateGoSchema(sref *openapi3.SchemaRef, path []string) (Schema, error) { // We have an object with no properties. This is a generic object // expressed as a map. outType = "map[string]interface{}" + setSkipOptionalPointerForContainerType(&outSchema) } else { // t == "" // If we don't even have the object designator, we're a completely // generic type. outType = "interface{}" + // this should never have an "optional pointer", as it doesn't make sense to be a `*interface{}` + outSchema.SkipOptionalPointer = true } outSchema.GoType = outType outSchema.DefineViaAlias = true @@ -385,6 +397,7 @@ func GenerateGoSchema(sref *openapi3.SchemaRef, path []string) (Schema, error) { // since we don't need them for a simple map. outSchema.HasAdditionalProperties = false outSchema.GoType = fmt.Sprintf("map[string]%s", additionalPropertiesType(outSchema)) + setSkipOptionalPointerForContainerType(&outSchema) return outSchema, nil } @@ -580,40 +593,34 @@ func oapiSchemaToGoType(schema *openapi3.Schema, path []string, outSchema *Schem if sliceContains(globalState.options.OutputOptions.DisableTypeAliasesForType, "array") { outSchema.DefineViaAlias = false } + setSkipOptionalPointerForContainerType(outSchema) } else if t.Is("integer") { // We default to int if format doesn't ask for something else. - if f == "int64" { - outSchema.GoType = "int64" - } else if f == "int32" { - outSchema.GoType = "int32" - } else if f == "int16" { - outSchema.GoType = "int16" - } else if f == "int8" { - outSchema.GoType = "int8" - } else if f == "int" { - outSchema.GoType = "int" - } else if f == "uint64" { - outSchema.GoType = "uint64" - } else if f == "uint32" { - outSchema.GoType = "uint32" - } else if f == "uint16" { - outSchema.GoType = "uint16" - } else if f == "uint8" { - outSchema.GoType = "uint8" - } else if f == "uint" { - outSchema.GoType = "uint" - } else { + switch f { + case "int64", + "int32", + "int16", + "int8", + "int", + "uint64", + "uint32", + "uint16", + "uint8", + "uint": + outSchema.GoType = f + default: outSchema.GoType = "int" } outSchema.DefineViaAlias = true } else if t.Is("number") { // We default to float for "number" - if f == "double" { + switch f { + case "double": outSchema.GoType = "float64" - } else if f == "float" || f == "" { + case "float", "": outSchema.GoType = "float32" - } else { + default: return fmt.Errorf("invalid number format: %s", f) } outSchema.DefineViaAlias = true @@ -628,6 +635,7 @@ func oapiSchemaToGoType(schema *openapi3.Schema, path []string, outSchema *Schem switch f { case "byte": outSchema.GoType = "[]byte" + setSkipOptionalPointerForContainerType(outSchema) case "email": outSchema.GoType = "openapi_types.Email" case "date": @@ -667,6 +675,13 @@ type FieldDescriptor struct { IsRef bool // Is this schema a reference to predefined object? } +func stringOrEmpty(b bool, s string) string { + if b { + return s + } + return "" +} + // GenFieldsFromProperties produce corresponding field names with JSON annotations, // given a list of schema descriptors func GenFieldsFromProperties(props []Property) []string { @@ -690,8 +705,8 @@ func GenFieldsFromProperties(props []Property) []string { // This comment has to be on its own line for godoc & IDEs to pick up var deprecationReason string if extension, ok := p.Extensions[extDeprecationReason]; ok { - if extOmitEmpty, err := extParseDeprecationReason(extension); err == nil { - deprecationReason = extOmitEmpty + if extDeprecationReason, err := extParseDeprecationReason(extension); err == nil { + deprecationReason = extDeprecationReason } } @@ -717,25 +732,37 @@ func GenFieldsFromProperties(props []Property) []string { omitEmpty = shouldOmitEmpty } - // Support x-omitempty + omitZero := false + + // default, but allow turning of + if shouldOmitEmpty && p.Schema.SkipOptionalPointer && globalState.options.OutputOptions.PreferSkipOptionalPointerWithOmitzero { + omitZero = true + } + + // Support x-omitempty and x-omitzero if extOmitEmptyValue, ok := p.Extensions[extPropOmitEmpty]; ok { - if extOmitEmpty, err := extParseOmitEmpty(extOmitEmptyValue); err == nil { - omitEmpty = extOmitEmpty + if xValue, err := extParseOmitEmpty(extOmitEmptyValue); err == nil { + omitEmpty = xValue + } + } + + if extOmitEmptyValue, ok := p.Extensions[extPropOmitZero]; ok { + if xValue, err := extParseOmitZero(extOmitEmptyValue); err == nil { + omitZero = xValue } } fieldTags := make(map[string]string) - if !omitEmpty { - fieldTags["json"] = p.JsonFieldName - if p.NeedsFormTag { - fieldTags["form"] = p.JsonFieldName - } - } else { - fieldTags["json"] = p.JsonFieldName + ",omitempty" - if p.NeedsFormTag { - fieldTags["form"] = p.JsonFieldName + ",omitempty" - } + fieldTags["json"] = p.JsonFieldName + + stringOrEmpty(omitEmpty, ",omitempty") + + stringOrEmpty(omitZero, ",omitzero") + + if globalState.options.OutputOptions.EnableYamlTags { + fieldTags["yaml"] = p.JsonFieldName + stringOrEmpty(omitEmpty, ",omitempty") + } + if p.NeedsFormTag { + fieldTags["form"] = p.JsonFieldName + stringOrEmpty(omitEmpty, ",omitempty") } // Support x-go-json-ignore @@ -889,3 +916,14 @@ func generateUnion(outSchema *Schema, elements openapi3.SchemaRefs, discriminato return nil } + +// setSkipOptionalPointerForContainerType ensures that the "optional pointer" is skipped on container types (such as a slice or a map). +// This is controlled using the `prefer-skip-optional-pointer-on-container-types` Output Option +// NOTE that it is still possible to override this on a per-field basis with `x-go-type-skip-optional-pointer` +func setSkipOptionalPointerForContainerType(outSchema *Schema) { + if !globalState.options.OutputOptions.PreferSkipOptionalPointerOnContainerTypes { + return + } + + outSchema.SkipOptionalPointer = true +} diff --git a/vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/server_urls.go b/vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/server_urls.go new file mode 100644 index 0000000..d10c2d9 --- /dev/null +++ b/vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/server_urls.go @@ -0,0 +1,81 @@ +package codegen + +import ( + "fmt" + "strconv" + "text/template" + + "github.com/getkin/kin-openapi/openapi3" +) + +const serverURLPrefix = "ServerUrl" +const serverURLSuffixIterations = 10 + +// ServerObjectDefinition defines the definition of an OpenAPI Server object (https://spec.openapis.org/oas/v3.0.3#server-object) as it is provided to code generation in `oapi-codegen` +type ServerObjectDefinition struct { + // GoName is the name of the variable for this Server URL + GoName string + + // OAPISchema is the underlying OpenAPI representation of the Server + OAPISchema *openapi3.Server +} + +func GenerateServerURLs(t *template.Template, spec *openapi3.T) (string, error) { + names := make(map[string]*openapi3.Server) + + for _, server := range spec.Servers { + suffix := server.Description + if suffix == "" { + suffix = nameNormalizer(server.URL) + } + name := serverURLPrefix + UppercaseFirstCharacter(suffix) + name = nameNormalizer(name) + + // if this is the only type with this name, store it + if _, conflict := names[name]; !conflict { + names[name] = server + continue + } + + // otherwise, try appending a number to the name + saved := false + // NOTE that we start at 1 on purpose, as + // + // ... ServerURLDevelopmentServer + // ... ServerURLDevelopmentServer1` + // + // reads better than: + // + // ... ServerURLDevelopmentServer + // ... ServerURLDevelopmentServer0 + for i := 1; i < 1+serverURLSuffixIterations; i++ { + suffixed := name + strconv.Itoa(i) + // and then store it if there's no conflict + if _, suffixConflict := names[suffixed]; !suffixConflict { + names[suffixed] = server + saved = true + break + } + } + + if saved { + continue + } + + // otherwise, error + return "", fmt.Errorf("failed to create a unique name for the Server URL (%#v) with description (%#v) after %d iterations", server.URL, server.Description, serverURLSuffixIterations) + } + + keys := SortedMapKeys(names) + servers := make([]ServerObjectDefinition, len(keys)) + i := 0 + for _, k := range keys { + servers[i] = ServerObjectDefinition{ + GoName: k, + OAPISchema: names[k], + } + i++ + } + + return GenerateTemplates([]string{"server-urls.tmpl"}, t, servers) +} diff --git a/vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/template_helpers.go b/vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/template_helpers.go index b9efe2c..49ee3ab 100644 --- a/vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/template_helpers.go +++ b/vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/template_helpers.go @@ -23,6 +23,7 @@ import ( "golang.org/x/text/cases" "golang.org/x/text/language" + "github.com/getkin/kin-openapi/openapi3" "github.com/oapi-codegen/oapi-codegen/v2/pkg/util" ) @@ -295,6 +296,26 @@ func stripNewLines(s string) string { return r.Replace(s) } +// genServerURLWithVariablesFunctionParams is a template helper method to generate the function parameters for the generated function for a Server object that contains `variables` (https://spec.openapis.org/oas/v3.0.3#server-object) +// +// goTypePrefix is the prefix being used to create underlying types in the template (likely the `ServerObjectDefinition.GoName`) +// variables are this `ServerObjectDefinition`'s variables for the Server object (likely the `ServerObjectDefinition.OAPISchema`) +func genServerURLWithVariablesFunctionParams(goTypePrefix string, variables map[string]*openapi3.ServerVariable) string { + keys := SortedMapKeys(variables) + + if len(variables) == 0 { + return "" + } + parts := make([]string, len(variables)) + + for i := range keys { + k := keys[i] + variableDefinitionPrefix := goTypePrefix + UppercaseFirstCharacter(k) + "Variable" + parts[i] = k + " " + variableDefinitionPrefix + } + return strings.Join(parts, ", ") +} + // TemplateFunctions is passed to the template engine, and we can call each // function here by keyName from the template code. var TemplateFunctions = template.FuncMap{ @@ -323,4 +344,6 @@ var TemplateFunctions = template.FuncMap{ "stripNewLines": stripNewLines, "sanitizeGoIdentity": SanitizeGoIdentity, "toGoComment": StringWithTypeNameToGoComment, + + "genServerURLWithVariablesFunctionParams": genServerURLWithVariablesFunctionParams, } diff --git a/vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/templates/additional-properties.tmpl b/vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/templates/additional-properties.tmpl index 7b7c0ac..2f45058 100644 --- a/vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/templates/additional-properties.tmpl +++ b/vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/templates/additional-properties.tmpl @@ -53,12 +53,12 @@ func (a {{.TypeName}}) MarshalJSON() ([]byte, error) { var err error object := make(map[string]json.RawMessage) {{range .Schema.Properties}} -{{if not .Required}}if a.{{.GoFieldName}} != nil { {{end}} +{{if .HasOptionalPointer}}if a.{{.GoFieldName}} != nil { {{end}} object["{{.JsonFieldName}}"], err = json.Marshal(a.{{.GoFieldName}}) if err != nil { return nil, fmt.Errorf("error marshaling '{{.JsonFieldName}}': %w", err) } -{{if not .Required}} }{{end}} +{{if .HasOptionalPointer}} }{{end}} {{end}} for fieldName, field := range a.AdditionalProperties { object[fieldName], err = json.Marshal(field) @@ -69,4 +69,4 @@ func (a {{.TypeName}}) MarshalJSON() ([]byte, error) { return json.Marshal(object) } {{end}} -{{end}} \ No newline at end of file +{{end}} diff --git a/vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/templates/chi/chi-middleware.tmpl b/vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/templates/chi/chi-middleware.tmpl index 9423a67..97866fc 100644 --- a/vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/templates/chi/chi-middleware.tmpl +++ b/vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/templates/chi/chi-middleware.tmpl @@ -58,7 +58,7 @@ func (siw *ServerInterfaceWrapper) {{$opid}}(w http.ResponseWriter, r *http.Requ if paramValue := r.URL.Query().Get("{{.ParamName}}"); paramValue != "" { {{if .IsPassThrough}} - params.{{.GoName}} = {{if not .Required}}&{{end}}paramValue + params.{{.GoName}} = {{if .HasOptionalPointer}}&{{end}}paramValue {{end}} {{if .IsJson}} @@ -69,7 +69,7 @@ func (siw *ServerInterfaceWrapper) {{$opid}}(w http.ResponseWriter, r *http.Requ return } - params.{{.GoName}} = {{if not .Required}}&{{end}}value + params.{{.GoName}} = {{if .HasOptionalPointer}}&{{end}}value {{end}} }{{if .Required}} else { siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "{{.ParamName}}"}) @@ -98,7 +98,7 @@ func (siw *ServerInterfaceWrapper) {{$opid}}(w http.ResponseWriter, r *http.Requ } {{if .IsPassThrough}} - params.{{.GoName}} = {{if not .Required}}&{{end}}valueList[0] + params.{{.GoName}} = {{if .HasOptionalPointer }}&{{end}}valueList[0] {{end}} {{if .IsJson}} @@ -117,7 +117,7 @@ func (siw *ServerInterfaceWrapper) {{$opid}}(w http.ResponseWriter, r *http.Requ } {{end}} - params.{{.GoName}} = {{if not .Required}}&{{end}}{{.GoName}} + params.{{.GoName}} = {{if .HasOptionalPointer}}&{{end}}{{.GoName}} } {{if .Required}}else { err := fmt.Errorf("Header parameter {{.ParamName}} is required, but not found") @@ -135,7 +135,7 @@ func (siw *ServerInterfaceWrapper) {{$opid}}(w http.ResponseWriter, r *http.Requ if cookie, err = r.Cookie("{{.ParamName}}"); err == nil { {{- if .IsPassThrough}} - params.{{.GoName}} = {{if not .Required}}&{{end}}cookie.Value + params.{{.GoName}} = {{if .HasOptionalPointer}}&{{end}}cookie.Value {{end}} {{- if .IsJson}} @@ -154,7 +154,7 @@ func (siw *ServerInterfaceWrapper) {{$opid}}(w http.ResponseWriter, r *http.Requ return } - params.{{.GoName}} = {{if not .Required}}&{{end}}value + params.{{.GoName}} = {{if .HasOptionalPointer}}&{{end}}value {{end}} {{- if .IsStyled}} @@ -164,7 +164,7 @@ func (siw *ServerInterfaceWrapper) {{$opid}}(w http.ResponseWriter, r *http.Requ siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "{{.ParamName}}", Err: err}) return } - params.{{.GoName}} = {{if not .Required}}&{{end}}value + params.{{.GoName}} = {{if .HasOptionalPointer}}&{{end}}value {{end}} } diff --git a/vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/templates/client-with-responses.tmpl b/vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/templates/client-with-responses.tmpl index 908f214..3b85500 100644 --- a/vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/templates/client-with-responses.tmpl +++ b/vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/templates/client-with-responses.tmpl @@ -74,6 +74,13 @@ func (r {{genResponseTypeName $opid | ucFirst}}) StatusCode() int { } return 0 } + +{{ if opts.OutputOptions.ClientResponseBytesFunction }} +// Bytes is a convenience method to retrieve the raw bytes from the HTTP response +func (r {{genResponseTypeName $opid | ucFirst}}) Bytes() []byte { + return r.Body +} +{{end}} {{end}} diff --git a/vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/templates/client.tmpl b/vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/templates/client.tmpl index 10ee564..822e110 100644 --- a/vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/templates/client.tmpl +++ b/vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/templates/client.tmpl @@ -197,12 +197,12 @@ func New{{$opid}}Request{{if .HasBody}}WithBody{{end}}(server string{{genParamAr if params != nil { queryValues := queryURL.Query() {{range $paramIdx, $param := .QueryParams}} - {{if not .Required}} if params.{{.GoName}} != nil { {{end}} + {{if .HasOptionalPointer}} if params.{{.GoName}} != nil { {{end}} {{if .IsPassThrough}} - queryValues.Add("{{.ParamName}}", {{if not .Required}}*{{end}}params.{{.GoName}}) + queryValues.Add("{{.ParamName}}", {{if .HasOptionalPointer}}*{{end}}params.{{.GoName}}) {{end}} {{if .IsJson}} - if queryParamBuf, err := json.Marshal({{if not .Required}}*{{end}}params.{{.GoName}}); err != nil { + if queryParamBuf, err := json.Marshal({{if .HasOptionalPointer}}*{{end}}params.{{.GoName}}); err != nil { return nil, err } else { queryValues.Add("{{.ParamName}}", string(queryParamBuf)) @@ -210,7 +210,7 @@ func New{{$opid}}Request{{if .HasBody}}WithBody{{end}}(server string{{genParamAr {{end}} {{if .IsStyled}} - if queryFrag, err := runtime.StyleParamWithLocation("{{.Style}}", {{.Explode}}, "{{.ParamName}}", runtime.ParamLocationQuery, {{if not .Required}}*{{end}}params.{{.GoName}}); err != nil { + if queryFrag, err := runtime.StyleParamWithLocation("{{.Style}}", {{.Explode}}, "{{.ParamName}}", runtime.ParamLocationQuery, {{if .HasOptionalPointer}}*{{end}}params.{{.GoName}}); err != nil { return nil, err } else if parsed, err := url.ParseQuery(queryFrag); err != nil { return nil, err @@ -222,7 +222,7 @@ func New{{$opid}}Request{{if .HasBody}}WithBody{{end}}(server string{{genParamAr } } {{end}} - {{if not .Required}}}{{end}} + {{if .HasOptionalPointer}}}{{end}} {{end}} queryURL.RawQuery = queryValues.Encode() } @@ -236,27 +236,27 @@ func New{{$opid}}Request{{if .HasBody}}WithBody{{end}}(server string{{genParamAr {{ if .HeaderParams }} if params != nil { {{range $paramIdx, $param := .HeaderParams}} - {{if not .Required}} if params.{{.GoName}} != nil { {{end}} + {{if .HasOptionalPointer}} if params.{{.GoName}} != nil { {{end}} var headerParam{{$paramIdx}} string {{if .IsPassThrough}} - headerParam{{$paramIdx}} = {{if not .Required}}*{{end}}params.{{.GoName}} + headerParam{{$paramIdx}} = {{if .HasOptionalPointer}}*{{end}}params.{{.GoName}} {{end}} {{if .IsJson}} var headerParamBuf{{$paramIdx}} []byte - headerParamBuf{{$paramIdx}}, err = json.Marshal({{if not .Required}}*{{end}}params.{{.GoName}}) + headerParamBuf{{$paramIdx}}, err = json.Marshal({{if .HasOptionalPointer}}*{{end}}params.{{.GoName}}) if err != nil { return nil, err } headerParam{{$paramIdx}} = string(headerParamBuf{{$paramIdx}}) {{end}} {{if .IsStyled}} - headerParam{{$paramIdx}}, err = runtime.StyleParamWithLocation("{{.Style}}", {{.Explode}}, "{{.ParamName}}", runtime.ParamLocationHeader, {{if not .Required}}*{{end}}params.{{.GoName}}) + headerParam{{$paramIdx}}, err = runtime.StyleParamWithLocation("{{.Style}}", {{.Explode}}, "{{.ParamName}}", runtime.ParamLocationHeader, {{if .HasOptionalPointer}}*{{end}}params.{{.GoName}}) if err != nil { return nil, err } {{end}} req.Header.Set("{{.ParamName}}", headerParam{{$paramIdx}}) - {{if not .Required}}}{{end}} + {{if .HasOptionalPointer}}}{{end}} {{end}} } {{- end }}{{/* if .HeaderParams */}} @@ -264,21 +264,21 @@ func New{{$opid}}Request{{if .HasBody}}WithBody{{end}}(server string{{genParamAr {{ if .CookieParams }} if params != nil { {{range $paramIdx, $param := .CookieParams}} - {{if not .Required}} if params.{{.GoName}} != nil { {{end}} + {{if .HasOptionalPointer}} if params.{{.GoName}} != nil { {{end}} var cookieParam{{$paramIdx}} string {{if .IsPassThrough}} - cookieParam{{$paramIdx}} = {{if not .Required}}*{{end}}params.{{.GoName}} + cookieParam{{$paramIdx}} = {{if .HasOptionalPointer}}*{{end}}params.{{.GoName}} {{end}} {{if .IsJson}} var cookieParamBuf{{$paramIdx}} []byte - cookieParamBuf{{$paramIdx}}, err = json.Marshal({{if not .Required}}*{{end}}params.{{.GoName}}) + cookieParamBuf{{$paramIdx}}, err = json.Marshal({{if .HasOptionalPointer}}*{{end}}params.{{.GoName}}) if err != nil { return nil, err } cookieParam{{$paramIdx}} = url.QueryEscape(string(cookieParamBuf{{$paramIdx}})) {{end}} {{if .IsStyled}} - cookieParam{{$paramIdx}}, err = runtime.StyleParamWithLocation("simple", {{.Explode}}, "{{.ParamName}}", runtime.ParamLocationCookie, {{if not .Required}}*{{end}}params.{{.GoName}}) + cookieParam{{$paramIdx}}, err = runtime.StyleParamWithLocation("simple", {{.Explode}}, "{{.ParamName}}", runtime.ParamLocationCookie, {{if .HasOptionalPointer}}*{{end}}params.{{.GoName}}) if err != nil { return nil, err } @@ -288,7 +288,7 @@ func New{{$opid}}Request{{if .HasBody}}WithBody{{end}}(server string{{genParamAr Value:cookieParam{{$paramIdx}}, } req.AddCookie(cookie{{$paramIdx}}) - {{if not .Required}}}{{end}} + {{if .HasOptionalPointer}}}{{end}} {{ end -}} } {{- end }}{{/* if .CookieParams */}} diff --git a/vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/templates/echo/echo-wrappers.tmpl b/vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/templates/echo/echo-wrappers.tmpl index 1603c3d..ea7b75e 100644 --- a/vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/templates/echo/echo-wrappers.tmpl +++ b/vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/templates/echo/echo-wrappers.tmpl @@ -44,7 +44,7 @@ func (w *ServerInterfaceWrapper) {{.OperationId}} (ctx echo.Context) error { {{else}} if paramValue := ctx.QueryParam("{{.ParamName}}"); paramValue != "" { {{if .IsPassThrough}} - params.{{.GoName}} = {{if not .Required}}&{{end}}paramValue + params.{{.GoName}} = {{if .HasOptionalPointer}}&{{end}}paramValue {{end}} {{if .IsJson}} var value {{.TypeDef}} @@ -52,7 +52,7 @@ func (w *ServerInterfaceWrapper) {{.OperationId}} (ctx echo.Context) error { if err != nil { return echo.NewHTTPError(http.StatusBadRequest, "Error unmarshaling parameter '{{.ParamName}}' as JSON") } - params.{{.GoName}} = {{if not .Required}}&{{end}}value + params.{{.GoName}} = {{if .HasOptionalPointer}}&{{end}}value {{end}} }{{if .Required}} else { return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Query argument {{.ParamName}} is required, but not found")) @@ -70,7 +70,7 @@ func (w *ServerInterfaceWrapper) {{.OperationId}} (ctx echo.Context) error { return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Expected one value for {{.ParamName}}, got %d", n)) } {{if .IsPassThrough}} - params.{{.GoName}} = {{if not .Required}}&{{end}}valueList[0] + params.{{.GoName}} = {{if .HasOptionalPointer}}&{{end}}valueList[0] {{end}} {{if .IsJson}} err = json.Unmarshal([]byte(valueList[0]), &{{.GoName}}) @@ -84,7 +84,7 @@ func (w *ServerInterfaceWrapper) {{.OperationId}} (ctx echo.Context) error { return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter {{.ParamName}}: %s", err)) } {{end}} - params.{{.GoName}} = {{if not .Required}}&{{end}}{{.GoName}} + params.{{.GoName}} = {{if .HasOptionalPointer}}&{{end}}{{.GoName}} } {{if .Required}}else { return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Header parameter {{.ParamName}} is required, but not found")) }{{end}} @@ -94,7 +94,7 @@ func (w *ServerInterfaceWrapper) {{.OperationId}} (ctx echo.Context) error { {{range .CookieParams}} if cookie, err := ctx.Cookie("{{.ParamName}}"); err == nil { {{if .IsPassThrough}} - params.{{.GoName}} = {{if not .Required}}&{{end}}cookie.Value + params.{{.GoName}} = {{if .HasOptionalPointer}}&{{end}}cookie.Value {{end}} {{if .IsJson}} var value {{.TypeDef}} @@ -107,7 +107,7 @@ func (w *ServerInterfaceWrapper) {{.OperationId}} (ctx echo.Context) error { if err != nil { return echo.NewHTTPError(http.StatusBadRequest, "Error unmarshaling parameter '{{.ParamName}}' as JSON") } - params.{{.GoName}} = {{if not .Required}}&{{end}}value + params.{{.GoName}} = {{if .HasOptionalPointer}}&{{end}}value {{end}} {{if .IsStyled}} var value {{.TypeDef}} @@ -115,7 +115,7 @@ func (w *ServerInterfaceWrapper) {{.OperationId}} (ctx echo.Context) error { if err != nil { return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter {{.ParamName}}: %s", err)) } - params.{{.GoName}} = {{if not .Required}}&{{end}}value + params.{{.GoName}} = {{if .HasOptionalPointer}}&{{end}}value {{end}} }{{if .Required}} else { return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Query argument {{.ParamName}} is required, but not found")) diff --git a/vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/templates/fiber/fiber-middleware.tmpl b/vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/templates/fiber/fiber-middleware.tmpl index 0904c78..e44f483 100644 --- a/vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/templates/fiber/fiber-middleware.tmpl +++ b/vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/templates/fiber/fiber-middleware.tmpl @@ -59,7 +59,7 @@ func (siw *ServerInterfaceWrapper) {{$opid}}(c *fiber.Ctx) error { if paramValue := c.Query("{{.ParamName}}"); paramValue != "" { {{if .IsPassThrough}} - params.{{.GoName}} = {{if not .Required}}&{{end}}paramValue + params.{{.GoName}} = {{if .HasOptionalPointer}}&{{end}}paramValue {{end}} {{if .IsJson}} @@ -69,7 +69,7 @@ func (siw *ServerInterfaceWrapper) {{$opid}}(c *fiber.Ctx) error { return fiber.NewError(fiber.StatusBadRequest, fmt.Errorf("Error unmarshaling parameter '{{.ParamName}}' as JSON: %w", err).Error()) } - params.{{.GoName}} = {{if not .Required}}&{{end}}value + params.{{.GoName}} = {{if .HasOptionalPointer}}&{{end}}value {{end}} }{{if .Required}} else { err = fmt.Errorf("Query argument {{.ParamName}} is required, but not found") @@ -93,7 +93,7 @@ func (siw *ServerInterfaceWrapper) {{$opid}}(c *fiber.Ctx) error { var {{.GoName}} {{.TypeDef}} {{if .IsPassThrough}} - params.{{.GoName}} = {{if not .Required}}&{{end}}value + params.{{.GoName}} = {{if .HasOptionalPointer}}&{{end}}value {{end}} {{if .IsJson}} @@ -110,7 +110,7 @@ func (siw *ServerInterfaceWrapper) {{$opid}}(c *fiber.Ctx) error { } {{end}} - params.{{.GoName}} = {{if not .Required}}&{{end}}{{.GoName}} + params.{{.GoName}} = {{if .HasOptionalPointer}}&{{end}}{{.GoName}} } {{if .Required}}else { err = fmt.Errorf("Header parameter {{.ParamName}} is required, but not found: %w", err) @@ -126,7 +126,7 @@ func (siw *ServerInterfaceWrapper) {{$opid}}(c *fiber.Ctx) error { if cookie = c.Cookies("{{.ParamName}}"); cookie == "" { {{- if .IsPassThrough}} - params.{{.GoName}} = {{if not .Required}}&{{end}}cookie + params.{{.GoName}} = {{if .HasOptionalPointer}}}&{{end}}cookie {{end}} {{- if .IsJson}} @@ -142,7 +142,7 @@ func (siw *ServerInterfaceWrapper) {{$opid}}(c *fiber.Ctx) error { return fiber.NewError(fiber.StatusBadRequest, fmt.Errorf("Error unmarshaling parameter '{{.ParamName}}' as JSON: %w", err).Error()) } - params.{{.GoName}} = {{if not .Required}}&{{end}}value + params.{{.GoName}} = {{if .HasOptionalPointer}}&{{end}}value {{end}} {{- if .IsStyled}} @@ -151,7 +151,7 @@ func (siw *ServerInterfaceWrapper) {{$opid}}(c *fiber.Ctx) error { if err != nil { return fiber.NewError(fiber.StatusBadRequest, fmt.Errorf("Invalid format for parameter {{.ParamName}}: %w", err).Error()) } - params.{{.GoName}} = {{if not .Required}}&{{end}}value + params.{{.GoName}} = {{if .HasOptionalPointer}}&{{end}}value {{end}} } diff --git a/vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/templates/gin/gin-wrappers.tmpl b/vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/templates/gin/gin-wrappers.tmpl index 8f1a6b2..3bc02e5 100644 --- a/vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/templates/gin/gin-wrappers.tmpl +++ b/vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/templates/gin/gin-wrappers.tmpl @@ -55,7 +55,7 @@ func (siw *ServerInterfaceWrapper) {{$opid}}(c *gin.Context) { if paramValue := c.Query("{{.ParamName}}"); paramValue != "" { {{if .IsPassThrough}} - params.{{.GoName}} = {{if not .Required}}&{{end}}paramValue + params.{{.GoName}} = {{if .HasOptionalPointer}}&{{end}}paramValue {{end}} {{if .IsJson}} @@ -66,7 +66,7 @@ func (siw *ServerInterfaceWrapper) {{$opid}}(c *gin.Context) { return } - params.{{.GoName}} = {{if not .Required}}&{{end}}value + params.{{.GoName}} = {{if .HasOptionalPointer}}&{{end}}value {{end}} }{{if .Required}} else { siw.ErrorHandler(c, fmt.Errorf("Query argument {{.ParamName}} is required, but not found"), http.StatusBadRequest) @@ -96,7 +96,7 @@ func (siw *ServerInterfaceWrapper) {{$opid}}(c *gin.Context) { } {{if .IsPassThrough}} - params.{{.GoName}} = {{if not .Required}}&{{end}}valueList[0] + params.{{.GoName}} = {{if .HasOptionalPointer}}&{{end}}valueList[0] {{end}} {{if .IsJson}} @@ -115,7 +115,7 @@ func (siw *ServerInterfaceWrapper) {{$opid}}(c *gin.Context) { } {{end}} - params.{{.GoName}} = {{if not .Required}}&{{end}}{{.GoName}} + params.{{.GoName}} = {{if .HasOptionalPointer}}&{{end}}{{.GoName}} } {{if .Required}}else { siw.ErrorHandler(c, fmt.Errorf("Header parameter {{.ParamName}} is required, but not found"), http.StatusBadRequest) @@ -132,7 +132,7 @@ func (siw *ServerInterfaceWrapper) {{$opid}}(c *gin.Context) { if cookie, err = c.Cookie("{{.ParamName}}"); err == nil { {{- if .IsPassThrough}} - params.{{.GoName}} = {{if not .Required}}&{{end}}cookie + params.{{.GoName}} = {{if .HasOptionalPointer}}&{{end}}cookie {{end}} {{- if .IsJson}} @@ -150,7 +150,7 @@ func (siw *ServerInterfaceWrapper) {{$opid}}(c *gin.Context) { return } - params.{{.GoName}} = {{if not .Required}}&{{end}}value + params.{{.GoName}} = {{if .HasOptionalPointer}}&{{end}}value {{end}} {{- if .IsStyled}} @@ -160,7 +160,7 @@ func (siw *ServerInterfaceWrapper) {{$opid}}(c *gin.Context) { siw.ErrorHandler(c, fmt.Errorf("Invalid format for parameter {{.ParamName}}: %w", err), http.StatusBadRequest) return } - params.{{.GoName}} = {{if not .Required}}&{{end}}value + params.{{.GoName}} = {{if .HasOptionalPointer}}&{{end}}value {{end}} } diff --git a/vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/templates/gorilla/gorilla-middleware.tmpl b/vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/templates/gorilla/gorilla-middleware.tmpl index 85b04c8..e8aa979 100644 --- a/vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/templates/gorilla/gorilla-middleware.tmpl +++ b/vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/templates/gorilla/gorilla-middleware.tmpl @@ -58,7 +58,7 @@ func (siw *ServerInterfaceWrapper) {{$opid}}(w http.ResponseWriter, r *http.Requ if paramValue := r.URL.Query().Get("{{.ParamName}}"); paramValue != "" { {{if .IsPassThrough}} - params.{{.GoName}} = {{if not .Required}}&{{end}}paramValue + params.{{.GoName}} = {{if .HasOptionalPointer}}&{{end}}paramValue {{end}} {{if .IsJson}} @@ -69,7 +69,7 @@ func (siw *ServerInterfaceWrapper) {{$opid}}(w http.ResponseWriter, r *http.Requ return } - params.{{.GoName}} = {{if not .Required}}&{{end}}value + params.{{.GoName}} = {{if .HasOptionalPointer}}&{{end}}value {{end}} }{{if .Required}} else { siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "{{.ParamName}}"}) @@ -98,7 +98,7 @@ func (siw *ServerInterfaceWrapper) {{$opid}}(w http.ResponseWriter, r *http.Requ } {{if .IsPassThrough}} - params.{{.GoName}} = {{if not .Required}}&{{end}}valueList[0] + params.{{.GoName}} = {{if .HasOptionalPointer}}&{{end}}valueList[0] {{end}} {{if .IsJson}} @@ -117,7 +117,7 @@ func (siw *ServerInterfaceWrapper) {{$opid}}(w http.ResponseWriter, r *http.Requ } {{end}} - params.{{.GoName}} = {{if not .Required}}&{{end}}{{.GoName}} + params.{{.GoName}} = {{if .HasOptionalPointer}}&{{end}}{{.GoName}} } {{if .Required}}else { err = fmt.Errorf("Header parameter {{.ParamName}} is required, but not found") @@ -135,7 +135,7 @@ func (siw *ServerInterfaceWrapper) {{$opid}}(w http.ResponseWriter, r *http.Requ if cookie, err = r.Cookie("{{.ParamName}}"); err == nil { {{- if .IsPassThrough}} - params.{{.GoName}} = {{if not .Required}}&{{end}}cookie.Value + params.{{.GoName}} = {{if .HasOptionalPointer}}&{{end}}cookie.Value {{end}} {{- if .IsJson}} @@ -154,7 +154,7 @@ func (siw *ServerInterfaceWrapper) {{$opid}}(w http.ResponseWriter, r *http.Requ return } - params.{{.GoName}} = {{if not .Required}}&{{end}}value + params.{{.GoName}} = {{if .HasOptionalPointer}}&{{end}}value {{end}} {{- if .IsStyled}} @@ -164,7 +164,7 @@ func (siw *ServerInterfaceWrapper) {{$opid}}(w http.ResponseWriter, r *http.Requ siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "{{.ParamName}}", Err: err}) return } - params.{{.GoName}} = {{if not .Required}}&{{end}}value + params.{{.GoName}} = {{if .HasOptionalPointer}}&{{end}}value {{end}} } diff --git a/vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/templates/iris/iris-middleware.tmpl b/vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/templates/iris/iris-middleware.tmpl index ae40439..814e6bc 100644 --- a/vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/templates/iris/iris-middleware.tmpl +++ b/vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/templates/iris/iris-middleware.tmpl @@ -55,7 +55,7 @@ func (w *ServerInterfaceWrapper) {{.OperationId}} (ctx iris.Context) { {{else}} if paramValue := ctx.QueryParam("{{.ParamName}}"); paramValue != "" { {{if .IsPassThrough}} - params.{{.GoName}} = {{if not .Required}}&{{end}}paramValue + params.{{.GoName}} = {{if .HasOptionalPointer}}&{{end}}paramValue {{end}} {{if .IsJson}} var value {{.TypeDef}} @@ -65,7 +65,7 @@ func (w *ServerInterfaceWrapper) {{.OperationId}} (ctx iris.Context) { ctx.WriteString("Error unmarshaling parameter '{{.ParamName}}' as JSON") return } - params.{{.GoName}} = {{if not .Required}}&{{end}}value + params.{{.GoName}} = {{if .HasOptionalPointer}}&{{end}}value {{end}} }{{if .Required}} else { ctx.StatusCode(http.StatusBadRequest) @@ -87,7 +87,7 @@ func (w *ServerInterfaceWrapper) {{.OperationId}} (ctx iris.Context) { return } {{if .IsPassThrough}} - params.{{.GoName}} = {{if not .Required}}&{{end}}valueList[0] + params.{{.GoName}} = {{if .HasOptionalPointer}}&{{end}}valueList[0] {{end}} {{if .IsJson}} err = json.Unmarshal([]byte(valueList[0]), &{{.GoName}}) @@ -105,7 +105,7 @@ func (w *ServerInterfaceWrapper) {{.OperationId}} (ctx iris.Context) { return } {{end}} - params.{{.GoName}} = {{if not .Required}}&{{end}}{{.GoName}} + params.{{.GoName}} = {{if .HasOptionalPointer}}&{{end}}{{.GoName}} } {{if .Required}}else { ctx.StatusCode(http.StatusBadRequest) ctx.WriteString("Header {{.ParamName}} is required, but not found") @@ -117,7 +117,7 @@ func (w *ServerInterfaceWrapper) {{.OperationId}} (ctx iris.Context) { {{range .CookieParams}} if cookie, err := ctx.Cookie("{{.ParamName}}"); err == nil { {{if .IsPassThrough}} - params.{{.GoName}} = {{if not .Required}}&{{end}}cookie.Value + params.{{.GoName}} = {{if .HasOptionalPointer}}&{{end}}cookie.Value {{end}} {{if .IsJson}} var value {{.TypeDef}} @@ -134,7 +134,7 @@ func (w *ServerInterfaceWrapper) {{.OperationId}} (ctx iris.Context) { ctx.WriteString("Error unmarshaling parameter '{{.ParamName}}' as JSON") return } - params.{{.GoName}} = {{if not .Required}}&{{end}}value + params.{{.GoName}} = {{if .HasOptionalPointer}}&{{end}}value {{end}} {{if .IsStyled}} var value {{.TypeDef}} @@ -144,7 +144,7 @@ func (w *ServerInterfaceWrapper) {{.OperationId}} (ctx iris.Context) { ctx.Writef("Invalid format for parameter {{.ParamName}}: %s", err) return } - params.{{.GoName}} = {{if not .Required}}&{{end}}value + params.{{.GoName}} = {{if .HasOptionalPointer}}&{{end}}value {{end}} }{{if .Required}} else { ctx.StatusCode(http.StatusBadRequest) diff --git a/vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/templates/server-urls.tmpl b/vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/templates/server-urls.tmpl new file mode 100644 index 0000000..f3599e5 --- /dev/null +++ b/vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/templates/server-urls.tmpl @@ -0,0 +1,61 @@ +{{ range . }} +{{ if eq 0 (len .OAPISchema.Variables) }} +{{/* URLs without variables are straightforward, so we'll create them a constant */}} +// {{ .GoName }} defines the Server URL for {{ .OAPISchema.Description }} +const {{ .GoName}} = "{{ .OAPISchema.URL }}" +{{ else }} +{{/* URLs with variables are not straightforward, as we may need multiple types, and so will model them as a function */}} + +{{/* first, we'll start by generating requisite types */}} + +{{ $goName := .GoName }} +{{ range $k, $v := .OAPISchema.Variables }} + {{ $prefix := printf "%s%sVariable" $goName ($k | ucFirst) }} + // {{ $prefix }} is the `{{ $k }}` variable for {{ $goName }} + type {{ $prefix }} string + {{ range $v.Enum }} + {{/* TODO this may result in broken generated code if any of the `enum` values are the literal value `default` https://github.com/oapi-codegen/oapi-codegen/issues/2003 */}} + // {{ $prefix }}{{ . | ucFirst }} is one of the accepted values for the `{{ $k }}` variable for {{ $goName }} + const {{ $prefix }}{{ . | ucFirst }} {{ $prefix }} = "{{ . }}" + {{ end }} + + {{/* TODO we should introduce a `Valid() error` method to enums https://github.com/oapi-codegen/oapi-codegen/issues/2006 */}} + + {{ if $v.Default }} + {{ if gt (len $v.Enum) 0 }} + {{/* if we have an enum, we should use the type defined for it for its default value + and reference the constant we've already defined for the value */}} + {{/* TODO this may result in broken generated code if any of the `enum` values are the literal value `default` https://github.com/oapi-codegen/oapi-codegen/issues/2003 */}} + {{/* TODO this may result in broken generated code if the `default` isn't found in `enum` (which is an issue with the spec) https://github.com/oapi-codegen/oapi-codegen/issues/2007 */}} + // {{ $prefix }}Default is the default choice, for the accepted values for the `{{ $k }}` variable for {{ $goName }} + const {{ $prefix }}Default {{ $prefix }} = {{ $prefix }}{{ $v.Default | ucFirst }} + {{ else }} + // {{ $prefix }}Default is the default value for the `{{ $k }}` variable for {{ $goName }} + const {{ $prefix }}Default = "{{ $v.Default }}" + {{ end }} + {{ end }} +{{ end }} + + +// New{{ .GoName }} constructs the Server URL for {{ .OAPISchema.Description }}, with the provided variables. +func New{{ .GoName }}({{ genServerURLWithVariablesFunctionParams .GoName .OAPISchema.Variables }}) (string, error) { + u := "{{ .OAPISchema.URL }}" + + {{ range $k, $v := .OAPISchema.Variables }} + {{- $placeholder := printf "{%s}" $k -}} + {{- if gt (len $v.Enum) 0 -}} + {{/* TODO https://github.com/oapi-codegen/oapi-codegen/issues/2006 */}} + // TODO in the future, this will validate that the value is part of the {{ printf "%s%sVariable" $goName ($k | ucFirst) }} enum + {{ end -}} + u = strings.ReplaceAll(u, "{{ $placeholder }}", string({{ $k }})) + {{ end }} + + if strings.Contains(u, "{") || strings.Contains(u, "}") { + return "", fmt.Errorf("after mapping variables, there were still `{` or `}` characters in the string: %#v", u) + } + + return u, nil +} + +{{ end }} +{{ end }} diff --git a/vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/templates/stdhttp/std-http-middleware.tmpl b/vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/templates/stdhttp/std-http-middleware.tmpl index 19f1fe2..0997735 100644 --- a/vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/templates/stdhttp/std-http-middleware.tmpl +++ b/vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/templates/stdhttp/std-http-middleware.tmpl @@ -58,7 +58,7 @@ func (siw *ServerInterfaceWrapper) {{$opid}}(w http.ResponseWriter, r *http.Requ if paramValue := r.URL.Query().Get("{{.ParamName}}"); paramValue != "" { {{if .IsPassThrough}} - params.{{.GoName}} = {{if not .Required}}&{{end}}paramValue + params.{{.GoName}} = {{if .HasOptionalPointer}}&{{end}}paramValue {{end}} {{if .IsJson}} @@ -69,7 +69,7 @@ func (siw *ServerInterfaceWrapper) {{$opid}}(w http.ResponseWriter, r *http.Requ return } - params.{{.GoName}} = {{if not .Required}}&{{end}}value + params.{{.GoName}} = {{if .HasOptionalPointer}}&{{end}}value {{end}} }{{if .Required}} else { siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "{{.ParamName}}"}) @@ -98,7 +98,7 @@ func (siw *ServerInterfaceWrapper) {{$opid}}(w http.ResponseWriter, r *http.Requ } {{if .IsPassThrough}} - params.{{.GoName}} = {{if not .Required}}&{{end}}valueList[0] + params.{{.GoName}} = {{if .HasOptionalPointer}}&{{end}}valueList[0] {{end}} {{if .IsJson}} @@ -117,7 +117,7 @@ func (siw *ServerInterfaceWrapper) {{$opid}}(w http.ResponseWriter, r *http.Requ } {{end}} - params.{{.GoName}} = {{if not .Required}}&{{end}}{{.GoName}} + params.{{.GoName}} = {{if .HasOptionalPointer}}&{{end}}{{.GoName}} } {{if .Required}}else { err := fmt.Errorf("Header parameter {{.ParamName}} is required, but not found") @@ -135,7 +135,7 @@ func (siw *ServerInterfaceWrapper) {{$opid}}(w http.ResponseWriter, r *http.Requ if cookie, err = r.Cookie("{{.ParamName}}"); err == nil { {{- if .IsPassThrough}} - params.{{.GoName}} = {{if not .Required}}&{{end}}cookie.Value + params.{{.GoName}} = {{if .HasOptionalPointer}}&{{end}}cookie.Value {{end}} {{- if .IsJson}} @@ -154,7 +154,7 @@ func (siw *ServerInterfaceWrapper) {{$opid}}(w http.ResponseWriter, r *http.Requ return } - params.{{.GoName}} = {{if not .Required}}&{{end}}value + params.{{.GoName}} = {{if .HasOptionalPointer}}&{{end}}value {{end}} {{- if .IsStyled}} @@ -164,7 +164,7 @@ func (siw *ServerInterfaceWrapper) {{$opid}}(w http.ResponseWriter, r *http.Requ siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "{{.ParamName}}", Err: err}) return } - params.{{.GoName}} = {{if not .Required}}&{{end}}value + params.{{.GoName}} = {{if .HasOptionalPointer}}&{{end}}value {{end}} } diff --git a/vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/templates/union-and-additional-properties.tmpl b/vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/templates/union-and-additional-properties.tmpl index 79b4c67..1c48092 100644 --- a/vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/templates/union-and-additional-properties.tmpl +++ b/vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/templates/union-and-additional-properties.tmpl @@ -54,12 +54,12 @@ func (a {{.TypeName}}) MarshalJSON() ([]byte, error) { } } {{range .Schema.Properties}} -{{if not .Required}}if a.{{.GoFieldName}} != nil { {{end}} +{{if .HasOptionalPointer}}if a.{{.GoFieldName}} != nil { {{end}} object["{{.JsonFieldName}}"], err = json.Marshal(a.{{.GoFieldName}}) if err != nil { return nil, fmt.Errorf("error marshaling '{{.JsonFieldName}}': %w", err) } -{{if not .Required}} }{{end}} +{{if .HasOptionalPointer}} }{{end}} {{end}} for fieldName, field := range a.AdditionalProperties { object[fieldName], err = json.Marshal(field) diff --git a/vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/templates/union.tmpl b/vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/templates/union.tmpl index 464fb11..c0385f8 100644 --- a/vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/templates/union.tmpl +++ b/vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/templates/union.tmpl @@ -102,12 +102,12 @@ } } {{range .Schema.Properties}} - {{if not .Required}}if t.{{.GoFieldName}} != nil { {{end}} + {{if .HasOptionalPointer}}if t.{{.GoFieldName}} != nil { {{end}} object["{{.JsonFieldName}}"], err = json.Marshal(t.{{.GoFieldName}}) if err != nil { return nil, fmt.Errorf("error marshaling '{{.JsonFieldName}}': %w", err) } - {{if not .Required}} }{{end}} + {{if .HasOptionalPointer}} }{{end}} {{end -}} b, err = json.Marshal(object) {{end -}} diff --git a/vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/utils.go b/vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/utils.go index e82d5e3..5326e67 100644 --- a/vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/utils.go +++ b/vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/utils.go @@ -283,7 +283,7 @@ func ToCamelCaseWithDigits(s string) string { func ToCamelCaseWithInitialisms(s string) string { parts := camelCaseMatchParts.FindAllString(ToCamelCaseWithDigits(s), -1) for i := range parts { - if v, ok := initialismsMap[strings.ToLower(parts[i])]; ok { + if v, ok := globalState.initialismsMap[strings.ToLower(parts[i])]; ok { parts[i] = v } } @@ -292,19 +292,26 @@ func ToCamelCaseWithInitialisms(s string) string { var camelCaseMatchParts = regexp.MustCompile(`[\p{Lu}\d]+([\p{Ll}\d]+|$)`) -// initialismsMap stores initialisms as "lower(initialism) -> initialism" map. -// List of initialisms was taken from https://staticcheck.io/docs/configuration/options/#initialisms. -var initialismsMap = makeInitialismsMap([]string{ +var initialismsList = []string{ "ACL", "API", "ASCII", "CPU", "CSS", "DNS", "EOF", "GUID", "HTML", "HTTP", "HTTPS", "ID", "IP", "JSON", "QPS", "RAM", "RPC", "SLA", "SMTP", "SQL", "SSH", "TCP", "TLS", "TTL", "UDP", "UI", "GID", "UID", "UUID", "URI", "URL", "UTF8", "VM", "XML", "XMPP", "XSRF", "XSS", "SIP", "RTP", "AMQP", "DB", "TS", -}) +} + +// targetWordRegex is a regex that matches all initialisms. +var targetWordRegex *regexp.Regexp + +func makeInitialismsMap(additionalInitialisms []string) map[string]string { + l := append(initialismsList, additionalInitialisms...) -func makeInitialismsMap(l []string) map[string]string { m := make(map[string]string, len(l)) for i := range l { m[strings.ToLower(l[i])] = l[i] } + + // Create a regex to match the initialisms + targetWordRegex = regexp.MustCompile(`(?i)(` + strings.Join(l, "|") + `)`) + return m } @@ -315,8 +322,6 @@ func ToCamelCaseWithInitialism(str string) string { func replaceInitialism(s string) string { // These strings do not apply CamelCase // Do not do CamelCase when these characters match when the preceding character is lowercase - // ["Acl", "Api", "Ascii", "Cpu", "Css", "Dns", "Eof", "Guid", "Html", "Http", "Https", "Id", "Ip", "Json", "Qps", "Ram", "Rpc", "Sla", "Smtp", "Sql", "Ssh", "Tcp", "Tls", "Ttl", "Udp", "Ui", "Gid", "Uid", "Uuid", "Uri", "Url", "Utf8", "Vm", "Xml", "Xmpp", "Xsrf", "Xss", "Sip", "Rtp", "Amqp", "Db", "Ts"] - targetWordRegex := regexp.MustCompile(`(?i)(Acl|Api|Ascii|Cpu|Css|Dns|Eof|Guid|Html|Http|Https|Id|Ip|Json|Qps|Ram|Rpc|Sla|Smtp|Sql|Ssh|Tcp|Tls|Ttl|Udp|Ui|Gid|Uid|Uuid|Uri|Url|Utf8|Vm|Xml|Xmpp|Xsrf|Xss|Sip|Rtp|Amqp|Db|Ts)`) return targetWordRegex.ReplaceAllStringFunc(s, func(s string) string { // If the preceding character is lowercase, do not do CamelCase if unicode.IsLower(rune(s[0])) { @@ -623,6 +628,12 @@ func SwaggerUriToGorillaUri(uri string) string { // {?param} // {?param*} func SwaggerUriToStdHttpUri(uri string) string { + // https://pkg.go.dev/net/http#hdr-Patterns-ServeMux + // The special wildcard {$} matches only the end of the URL. For example, the pattern "/{$}" matches only the path "/", whereas the pattern "/" matches every path. + if uri == "/" { + return "/{$}" + } + return pathParamRE.ReplaceAllString(uri, "{$1}") } @@ -805,6 +816,8 @@ func typeNamePrefix(name string) (prefix string) { prefix += "Caret" case '%': prefix += "Percent" + case '_': + prefix += "Underscore" default: // Prepend "N" to schemas starting with a number if prefix == "" && unicode.IsDigit(r) { @@ -868,6 +881,8 @@ func DeprecationComment(reason string) string { content := "Deprecated:" // The colon is required at the end even without reason if reason != "" { content += fmt.Sprintf(" %s", reason) + } else { + content += " this property has been marked as deprecated upstream, but no `x-deprecated-reason` was set" } return stringToGoCommentWithPrefix(content, "") @@ -1086,7 +1101,7 @@ func isAdditionalPropertiesExplicitFalse(s *openapi3.Schema) bool { return false } - return *s.AdditionalProperties.Has == false //nolint:gosimple + return *s.AdditionalProperties.Has == false //nolint:staticcheck } func sliceContains[E comparable](s []E, v E) bool { diff --git a/vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/util/loader.go b/vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/util/loader.go index b10e594..89830a8 100644 --- a/vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/util/loader.go +++ b/vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/util/loader.go @@ -4,6 +4,7 @@ import ( "bytes" "fmt" "net/url" + "path/filepath" "strings" "github.com/getkin/kin-openapi/openapi3" @@ -65,29 +66,34 @@ func LoadSwaggerWithOverlay(filePath string, opts LoadSwaggerWithOverlayOpts) (s err = overlay.Validate() if err != nil { - return nil, fmt.Errorf("The Overlay in %#v was not valid: %v", opts.Path, err) + return nil, fmt.Errorf("the Overlay in %#v was not valid: %v", opts.Path, err) } if opts.Strict { err, vs := overlay.ApplyToStrict(&node) if err != nil { - return nil, fmt.Errorf("Failed to apply Overlay %#v to specification %#v: %v\nAdditionally, the following validation errors were found:\n- %s", opts.Path, filePath, err, strings.Join(vs, "\n- ")) + return nil, fmt.Errorf("failed to apply Overlay %#v to specification %#v: %v\nAdditionally, the following validation errors were found:\n- %s", opts.Path, filePath, err, strings.Join(vs, "\n- ")) } } else { err = overlay.ApplyTo(&node) if err != nil { - return nil, fmt.Errorf("Failed to apply Overlay %#v to specification %#v: %v", opts.Path, filePath, err) + return nil, fmt.Errorf("failed to apply Overlay %#v to specification %#v: %v", opts.Path, filePath, err) } } b, err := yaml.Marshal(&node) if err != nil { - return nil, fmt.Errorf("Failed to serialize Overlay'd specification %#v: %v", opts.Path, err) + return nil, fmt.Errorf("failed to serialize Overlay'd specification %#v: %v", opts.Path, err) } - swagger, err = openapi3.NewLoader().LoadFromData(b) + loader := openapi3.NewLoader() + loader.IsExternalRefsAllowed = true + + swagger, err = loader.LoadFromDataWithPath(b, &url.URL{ + Path: filepath.ToSlash(filePath), + }) if err != nil { - return nil, fmt.Errorf("Failed to serialize Overlay'd specification %#v: %v", opts.Path, err) + return nil, fmt.Errorf("failed to serialize Overlay'd specification %#v: %v", opts.Path, err) } return swagger, nil diff --git a/vendor/github.com/speakeasy-api/jsonpath/LICENSE b/vendor/github.com/speakeasy-api/jsonpath/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/vendor/github.com/speakeasy-api/jsonpath/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/vendor/github.com/speakeasy-api/jsonpath/pkg/jsonpath/config/config.go b/vendor/github.com/speakeasy-api/jsonpath/pkg/jsonpath/config/config.go new file mode 100644 index 0000000..bd8286c --- /dev/null +++ b/vendor/github.com/speakeasy-api/jsonpath/pkg/jsonpath/config/config.go @@ -0,0 +1,31 @@ +package config + +type Option func(*config) + +// WithPropertyNameExtension enables the use of the "~" character to access a property key. +// It is not enabled by default as this is outside of RFC 9535, but is important for several use-cases +func WithPropertyNameExtension() Option { + return func(cfg *config) { + cfg.propertyNameExtension = true + } +} + +type Config interface { + PropertyNameEnabled() bool +} + +type config struct { + propertyNameExtension bool +} + +func (c *config) PropertyNameEnabled() bool { + return c.propertyNameExtension +} + +func New(opts ...Option) Config { + cfg := &config{} + for _, opt := range opts { + opt(cfg) + } + return cfg +} diff --git a/vendor/github.com/speakeasy-api/jsonpath/pkg/jsonpath/filter.go b/vendor/github.com/speakeasy-api/jsonpath/pkg/jsonpath/filter.go new file mode 100644 index 0000000..0f5b77f --- /dev/null +++ b/vendor/github.com/speakeasy-api/jsonpath/pkg/jsonpath/filter.go @@ -0,0 +1,456 @@ +package jsonpath + +import ( + "gopkg.in/yaml.v3" + "strconv" + "strings" +) + +// filter-selector = "?" S logical-expr +type filterSelector struct { + // logical-expr = logical-or-expr + expression *logicalOrExpr +} + +func (s filterSelector) ToString() string { + return s.expression.ToString() +} + +// logical-or-expr = logical-and-expr *(S "||" S logical-and-expr) +type logicalOrExpr struct { + expressions []*logicalAndExpr +} + +func (e logicalOrExpr) ToString() string { + builder := strings.Builder{} + for i, expr := range e.expressions { + if i > 0 { + builder.WriteString(" || ") + } + builder.WriteString(expr.ToString()) + } + return builder.String() +} + +// logical-and-expr = basic-expr *(S "&&" S basic-expr) +type logicalAndExpr struct { + expressions []*basicExpr +} + +func (e logicalAndExpr) ToString() string { + builder := strings.Builder{} + for i, expr := range e.expressions { + if i > 0 { + builder.WriteString(" && ") + } + builder.WriteString(expr.ToString()) + } + return builder.String() +} + +// relQuery rel-query = current-node-identifier segments +// current-node-identifier = "@" +type relQuery struct { + segments []*segment +} + +func (q relQuery) ToString() string { + builder := strings.Builder{} + builder.WriteString("@") + for _, segment := range q.segments { + builder.WriteString(segment.ToString()) + } + return builder.String() +} + +// filterQuery filter-query = rel-query / jsonpath-query +type filterQuery struct { + relQuery *relQuery + jsonPathQuery *jsonPathAST +} + +func (q filterQuery) ToString() string { + if q.relQuery != nil { + return q.relQuery.ToString() + } else if q.jsonPathQuery != nil { + return q.jsonPathQuery.ToString() + } + return "" +} + +// functionArgument function-argument = literal / +// +// filter-query / ; (includes singular-query) +// logical-expr / +// function-expr +type functionArgument struct { + literal *literal + filterQuery *filterQuery + logicalExpr *logicalOrExpr + functionExpr *functionExpr +} + +type functionArgType int + +const ( + functionArgTypeLiteral functionArgType = iota + functionArgTypeNodes +) + +type resolvedArgument struct { + kind functionArgType + literal *literal + nodes []*literal +} + +func (a functionArgument) Eval(idx index, node *yaml.Node, root *yaml.Node) resolvedArgument { + if a.literal != nil { + return resolvedArgument{kind: functionArgTypeLiteral, literal: a.literal} + } else if a.filterQuery != nil { + result := a.filterQuery.Query(idx, node, root) + lits := make([]*literal, len(result)) + for i, node := range result { + lit := nodeToLiteral(node) + lits[i] = &lit + } + if len(result) != 1 { + return resolvedArgument{kind: functionArgTypeNodes, nodes: lits} + } else { + return resolvedArgument{kind: functionArgTypeLiteral, literal: lits[0]} + } + } else if a.logicalExpr != nil { + res := a.logicalExpr.Matches(idx, node, root) + return resolvedArgument{kind: functionArgTypeLiteral, literal: &literal{bool: &res}} + } else if a.functionExpr != nil { + res := a.functionExpr.Evaluate(idx, node, root) + return resolvedArgument{kind: functionArgTypeLiteral, literal: &res} + } + return resolvedArgument{} +} + +func (a functionArgument) ToString() string { + builder := strings.Builder{} + if a.literal != nil { + builder.WriteString(a.literal.ToString()) + } else if a.filterQuery != nil { + builder.WriteString(a.filterQuery.ToString()) + } else if a.logicalExpr != nil { + builder.WriteString(a.logicalExpr.ToString()) + } else if a.functionExpr != nil { + builder.WriteString(a.functionExpr.ToString()) + } + return builder.String() +} + +//function-name = function-name-first *function-name-char +//function-name-first = LCALPHA +//function-name-char = function-name-first / "_" / DIGIT +//LCALPHA = %x61-7A ; "a".."z" +// + +type functionType int + +const ( + functionTypeLength functionType = iota + functionTypeCount + functionTypeMatch + functionTypeSearch + functionTypeValue +) + +var functionTypeMap = map[string]functionType{ + "length": functionTypeLength, + "count": functionTypeCount, + "match": functionTypeMatch, + "search": functionTypeSearch, + "value": functionTypeValue, +} + +func (f functionType) String() string { + for k, v := range functionTypeMap { + if v == f { + return k + } + } + return "unknown" +} + +// functionExpr function-expr = function-name "(" S [function-argument +// *(S "," S function-argument)] S ")" +type functionExpr struct { + funcType functionType + args []*functionArgument +} + +func (e functionExpr) ToString() string { + builder := strings.Builder{} + builder.WriteString(e.funcType.String()) + builder.WriteString("(") + for i, arg := range e.args { + if i > 0 { + builder.WriteString(", ") + } + builder.WriteString(arg.ToString()) + } + builder.WriteString(")") + return builder.String() +} + +// testExpr test-expr = [logical-not-op S] +// +// (filter-query / ; existence/non-existence +// function-expr) ; LogicalType or NodesType +type testExpr struct { + not bool + filterQuery *filterQuery + functionExpr *functionExpr +} + +func (e testExpr) ToString() string { + builder := strings.Builder{} + if e.not { + builder.WriteString("!") + } + if e.filterQuery != nil { + builder.WriteString(e.filterQuery.ToString()) + } else if e.functionExpr != nil { + builder.WriteString(e.functionExpr.ToString()) + } + return builder.String() +} + +// basicExpr basic-expr = +// +// paren-expr / +// comparison-expr / +// test-expr +type basicExpr struct { + parenExpr *parenExpr + comparisonExpr *comparisonExpr + testExpr *testExpr +} + +func (e basicExpr) ToString() string { + if e.parenExpr != nil { + return e.parenExpr.ToString() + } else if e.comparisonExpr != nil { + return e.comparisonExpr.ToString() + } else if e.testExpr != nil { + return e.testExpr.ToString() + } + return "" +} + +// literal literal = number / +// . string-literal / +// . true / false / null +type literal struct { + // we generally decompose these into their component parts for easier evaluation + integer *int + float64 *float64 + string *string + bool *bool + null *bool + node *yaml.Node +} + +func (l literal) ToString() string { + if l.integer != nil { + return strconv.Itoa(*l.integer) + } else if l.float64 != nil { + return strconv.FormatFloat(*l.float64, 'f', -1, 64) + } else if l.string != nil { + builder := strings.Builder{} + builder.WriteString("'") + builder.WriteString(escapeString(*l.string)) + builder.WriteString("'") + return builder.String() + } else if l.bool != nil { + if *l.bool { + return "true" + } else { + return "false" + } + } else if l.null != nil { + if *l.null { + return "null" + } else { + return "null" + } + } else if l.node != nil { + switch l.node.Kind { + case yaml.ScalarNode: + return l.node.Value + case yaml.SequenceNode: + builder := strings.Builder{} + builder.WriteString("[") + for i, child := range l.node.Content { + if i > 0 { + builder.WriteString(",") + } + builder.WriteString(literal{node: child}.ToString()) + } + builder.WriteString("]") + return builder.String() + case yaml.MappingNode: + builder := strings.Builder{} + builder.WriteString("{") + for i, child := range l.node.Content { + if i > 0 { + builder.WriteString(",") + } + builder.WriteString(literal{node: child}.ToString()) + } + builder.WriteString("}") + return builder.String() + } + } + return "" +} + +func escapeString(value string) string { + b := strings.Builder{} + for i := 0; i < len(value); i++ { + if value[i] == '\n' { + b.WriteString("\\\\n") + } else if value[i] == '\\' { + b.WriteString("\\\\") + } else if value[i] == '\'' { + b.WriteString("\\'") + } else { + b.WriteByte(value[i]) + } + } + return b.String() +} + +type absQuery jsonPathAST + +func (q absQuery) ToString() string { + builder := strings.Builder{} + builder.WriteString("$") + for _, segment := range q.segments { + builder.WriteString(segment.ToString()) + } + return builder.String() +} + +// singularQuery singular-query = rel-singular-query / abs-singular-query +type singularQuery struct { + relQuery *relQuery + absQuery *absQuery +} + +func (q singularQuery) ToString() string { + if q.relQuery != nil { + return q.relQuery.ToString() + } else if q.absQuery != nil { + return q.absQuery.ToString() + } + return "" +} + +// comparable +// +// comparable = literal / +// singular-query / ; singular query value +// function-expr ; ValueType +type comparable struct { + literal *literal + singularQuery *singularQuery + functionExpr *functionExpr +} + +func (c comparable) ToString() string { + if c.literal != nil { + return c.literal.ToString() + } else if c.singularQuery != nil { + return c.singularQuery.ToString() + } else if c.functionExpr != nil { + return c.functionExpr.ToString() + } + return "" +} + +// comparisonExpr represents a comparison expression +// +// comparison-expr = comparable S comparison-op S comparable +// literal = number / string-literal / +// true / false / null +// comparable = literal / +// singular-query / ; singular query value +// function-expr ; ValueType +// comparison-op = "==" / "!=" / +// "<=" / ">=" / +// "<" / ">" +type comparisonExpr struct { + left *comparable + op comparisonOperator + right *comparable +} + +func (e comparisonExpr) ToString() string { + builder := strings.Builder{} + builder.WriteString(e.left.ToString()) + builder.WriteString(" ") + builder.WriteString(e.op.ToString()) + builder.WriteString(" ") + builder.WriteString(e.right.ToString()) + return builder.String() +} + +// existExpr represents an existence expression +type existExpr struct { + query string +} + +// parenExpr represents a parenthesized expression +// +// paren-expr = [logical-not-op S] "(" S logical-expr S ")" +type parenExpr struct { + // "!" + not bool + // "(" logicalOrExpr ")" + expr *logicalOrExpr +} + +func (e parenExpr) ToString() string { + builder := strings.Builder{} + if e.not { + builder.WriteString("!") + } + builder.WriteString("(") + builder.WriteString(e.expr.ToString()) + builder.WriteString(")") + return builder.String() +} + +// comparisonOperator represents a comparison operator +type comparisonOperator int + +const ( + equalTo comparisonOperator = iota + notEqualTo + lessThan + lessThanEqualTo + greaterThan + greaterThanEqualTo +) + +func (o comparisonOperator) ToString() string { + switch o { + case equalTo: + return "==" + case notEqualTo: + return "!=" + case lessThan: + return "<" + case lessThanEqualTo: + return "<=" + case greaterThan: + return ">" + case greaterThanEqualTo: + return ">=" + } + return "" +} diff --git a/vendor/github.com/speakeasy-api/jsonpath/pkg/jsonpath/jsonpath.go b/vendor/github.com/speakeasy-api/jsonpath/pkg/jsonpath/jsonpath.go new file mode 100644 index 0000000..d5c8a5b --- /dev/null +++ b/vendor/github.com/speakeasy-api/jsonpath/pkg/jsonpath/jsonpath.go @@ -0,0 +1,35 @@ +package jsonpath + +import ( + "fmt" + "github.com/speakeasy-api/jsonpath/pkg/jsonpath/config" + "github.com/speakeasy-api/jsonpath/pkg/jsonpath/token" + "gopkg.in/yaml.v3" +) + +func NewPath(input string, opts ...config.Option) (*JSONPath, error) { + tokenizer := token.NewTokenizer(input, opts...) + tokens := tokenizer.Tokenize() + for i := 0; i < len(tokens); i++ { + if tokens[i].Token == token.ILLEGAL { + return nil, fmt.Errorf(tokenizer.ErrorString(&tokens[i], "unexpected token")) + } + } + parser := newParserPrivate(tokenizer, tokens, opts...) + err := parser.parse() + if err != nil { + return nil, err + } + return parser, nil +} + +func (p *JSONPath) Query(root *yaml.Node) []*yaml.Node { + return p.ast.Query(root, root) +} + +func (p *JSONPath) String() string { + if p == nil { + return "" + } + return p.ast.ToString() +} diff --git a/vendor/github.com/speakeasy-api/jsonpath/pkg/jsonpath/parser.go b/vendor/github.com/speakeasy-api/jsonpath/pkg/jsonpath/parser.go new file mode 100644 index 0000000..ae6e7f1 --- /dev/null +++ b/vendor/github.com/speakeasy-api/jsonpath/pkg/jsonpath/parser.go @@ -0,0 +1,729 @@ +package jsonpath + +import ( + "errors" + "fmt" + "github.com/speakeasy-api/jsonpath/pkg/jsonpath/config" + "github.com/speakeasy-api/jsonpath/pkg/jsonpath/token" + "strconv" + "strings" +) + +const MaxSafeFloat int64 = 9007199254740991 + +type mode int + +const ( + modeNormal mode = iota + modeSingular +) + +// JSONPath represents a JSONPath parser. +type JSONPath struct { + tokenizer *token.Tokenizer + tokens []token.TokenInfo + ast jsonPathAST + current int + mode []mode + config config.Config +} + +// newParserPrivate creates a new JSONPath with the given tokens. +func newParserPrivate(tokenizer *token.Tokenizer, tokens []token.TokenInfo, opts ...config.Option) *JSONPath { + return &JSONPath{tokenizer, tokens, jsonPathAST{}, 0, []mode{modeNormal}, config.New(opts...)} +} + +// parse parses the JSONPath tokens and returns the root node of the AST. +// +// jsonpath-query = root-identifier segments +func (p *JSONPath) parse() error { + if len(p.tokens) == 0 { + return fmt.Errorf("empty JSONPath expression") + } + + if p.tokens[p.current].Token != token.ROOT { + return p.parseFailure(&p.tokens[p.current], "expected '$'") + } + p.current++ + + for p.current < len(p.tokens) { + segment, err := p.parseSegment() + if err != nil { + return err + } + p.ast.segments = append(p.ast.segments, segment) + } + return nil +} + +func (p *JSONPath) parseFailure(target *token.TokenInfo, msg string) error { + return errors.New(p.tokenizer.ErrorString(target, msg)) +} + +// peek returns true if the upcoming token matches the given token type. +func (p *JSONPath) peek(token token.Token) bool { + return p.current+1 < len(p.tokens) && p.tokens[p.current+1].Token == token +} + +// peek returns true if the upcoming token matches the given token type. +func (p *JSONPath) next(token token.Token) bool { + return p.current < len(p.tokens) && p.tokens[p.current].Token == token +} + +// expect consumes the current token if it matches the given token type. +func (p *JSONPath) expect(token token.Token) bool { + if p.peek(token) { + p.current++ + return true + } + return false +} + +// isComparisonOperator returns true if the given token is a comparison operator. +func (p *JSONPath) isComparisonOperator(tok token.Token) bool { + return tok == token.EQ || tok == token.NE || tok == token.GT || tok == token.GE || tok == token.LT || tok == token.LE +} + +func (p *JSONPath) parseSegment() (*segment, error) { + currentToken := p.tokens[p.current] + if currentToken.Token == token.RECURSIVE { + if p.mode[len(p.mode)-1] == modeSingular { + return nil, p.parseFailure(&p.tokens[p.current], "unexpected recursive descent in singular query") + } + p.current++ + child, err := p.parseInnerSegment() + if err != nil { + return nil, err + } + return &segment{kind: segmentKindDescendant, descendant: child}, nil + } else if currentToken.Token == token.CHILD || currentToken.Token == token.BRACKET_LEFT { + if currentToken.Token == token.CHILD { + p.current++ + } + child, err := p.parseInnerSegment() + if err != nil { + return nil, err + } + return &segment{kind: segmentKindChild, child: child}, nil + } else if p.config.PropertyNameEnabled() && currentToken.Token == token.PROPERTY_NAME { + p.current++ + return &segment{kind: segmentKindProperyName}, nil + } + return nil, p.parseFailure(¤tToken, "unexpected token when parsing segment") +} + +func (p *JSONPath) parseInnerSegment() (retValue *innerSegment, err error) { + defer func() { + if p.mode[len(p.mode)-1] == modeSingular && retValue != nil { + if len(retValue.selectors) > 1 { + retValue = nil + err = p.parseFailure(&p.tokens[p.current], "unexpected multiple selectors in singular query") + return + } else if retValue.kind == segmentDotWildcard { + retValue = nil + err = p.parseFailure(&p.tokens[p.current], "unexpected wildcard in singular query") + return + } + } + }() + // .* + // .STRING + // [] + if p.current >= len(p.tokens) { + return nil, p.parseFailure(nil, "unexpected end of input") + } + firstToken := p.tokens[p.current] + if firstToken.Token == token.WILDCARD { + p.current += 1 + return &innerSegment{segmentDotWildcard, "", nil}, nil + } else if firstToken.Token == token.STRING { + dotName := p.tokens[p.current].Literal + p.current += 1 + return &innerSegment{segmentDotMemberName, dotName, nil}, nil + } else if firstToken.Token == token.BRACKET_LEFT { + prior := p.current + p.current += 1 + selectors := []*selector{} + for p.current < len(p.tokens) { + innerSelector, err := p.parseSelector() + if err != nil { + p.current = prior + return nil, err + } + selectors = append(selectors, innerSelector) + if len(p.tokens) <= p.current { + return nil, p.parseFailure(&p.tokens[p.current-1], "unexpected end of input") + } + if p.tokens[p.current].Token == token.BRACKET_RIGHT { + break + } else if p.tokens[p.current].Token == token.COMMA { + p.current++ + } + } + if p.tokens[p.current].Token != token.BRACKET_RIGHT { + prior = p.current + return nil, p.parseFailure(&p.tokens[p.current], "expected ']'") + } + p.current += 1 + return &innerSegment{kind: segmentLongHand, dotName: "", selectors: selectors}, nil + } + return nil, p.parseFailure(&firstToken, "unexpected token when parsing inner segment") +} + +func (p *JSONPath) parseSelector() (retSelector *selector, err error) { + //selector = name-selector / + // wildcard-selector / + // slice-selector / + // index-selector / + // filter-selector + initial := p.current + defer func() { + if p.mode[len(p.mode)-1] == modeSingular && retSelector != nil { + if retSelector.kind == selectorSubKindWildcard { + err = p.parseFailure(&p.tokens[initial], "unexpected wildcard in singular query") + retSelector = nil + } else if retSelector.kind == selectorSubKindArraySlice { + err = p.parseFailure(&p.tokens[initial], "unexpected slice in singular query") + retSelector = nil + } + } + }() + + // name-selector = string-literal + if p.tokens[p.current].Token == token.STRING_LITERAL { + name := p.tokens[p.current].Literal + p.current++ + return &selector{kind: selectorSubKindName, name: name}, nil + // wildcard-selector = "*" + } else if p.tokens[p.current].Token == token.WILDCARD { + p.current++ + return &selector{kind: selectorSubKindWildcard}, nil + } else if p.tokens[p.current].Token == token.INTEGER { + // peek ahead to see if it's a slice + if p.peek(token.ARRAY_SLICE) { + slice, err := p.parseSliceSelector() + if err != nil { + return nil, err + } + return &selector{kind: selectorSubKindArraySlice, slice: slice}, nil + } + // peek ahead to see if we close the array index properly + if !p.peek(token.BRACKET_RIGHT) && !p.peek(token.COMMA) { + return nil, p.parseFailure(&p.tokens[p.current], "expected ']' or ','") + } + // else it's an index + lit := p.tokens[p.current].Literal + // make sure it's not -0 + if lit == "-0" { + return nil, p.parseFailure(&p.tokens[p.current], "-0 unexpected") + } + // make sure lit is an integer + i, err := strconv.ParseInt(lit, 10, 64) + if err != nil { + return nil, p.parseFailure(&p.tokens[p.current], "expected an integer") + } + err = p.checkSafeInteger(i, lit) + if err != nil { + return nil, err + } + + p.current++ + + return &selector{kind: selectorSubKindArrayIndex, index: i}, nil + } else if p.tokens[p.current].Token == token.ARRAY_SLICE { + slice, err := p.parseSliceSelector() + if err != nil { + return nil, err + } + return &selector{kind: selectorSubKindArraySlice, slice: slice}, nil + } else if p.tokens[p.current].Token == token.FILTER { + return p.parseFilterSelector() + } + + return nil, p.parseFailure(&p.tokens[p.current], "unexpected token when parsing selector") +} + +func (p *JSONPath) parseSliceSelector() (*slice, error) { + // slice-selector = [start S] ":" S [end S] [":" [S step]] + var start, end, step *int64 + + // parse the start index + if p.tokens[p.current].Token == token.INTEGER { + literal := p.tokens[p.current].Literal + i, err := strconv.ParseInt(literal, 10, 64) + if err != nil { + return nil, p.parseFailure(&p.tokens[p.current], "expected an integer") + } + err = p.checkSafeInteger(i, literal) + if err != nil { + return nil, err + } + + start = &i + p.current += 1 + } + + // Expect a colon + if p.tokens[p.current].Token != token.ARRAY_SLICE { + return nil, p.parseFailure(&p.tokens[p.current], "expected ':'") + } + p.current++ + + // parse the end index + if p.tokens[p.current].Token == token.INTEGER { + literal := p.tokens[p.current].Literal + i, err := strconv.ParseInt(literal, 10, 64) + if err != nil { + return nil, p.parseFailure(&p.tokens[p.current], "expected an integer") + } + err = p.checkSafeInteger(i, literal) + if err != nil { + return nil, err + } + + end = &i + p.current++ + } + + // Check for an optional second colon and step value + if p.tokens[p.current].Token == token.ARRAY_SLICE { + p.current++ + if p.tokens[p.current].Token == token.INTEGER { + literal := p.tokens[p.current].Literal + i, err := strconv.ParseInt(literal, 10, 64) + if err != nil { + return nil, p.parseFailure(&p.tokens[p.current], "expected an integer") + } + err = p.checkSafeInteger(i, literal) + if err != nil { + return nil, err + } + + step = &i + p.current++ + } + } + if p.tokens[p.current].Token != token.BRACKET_RIGHT { + return nil, p.parseFailure(&p.tokens[p.current], "expected ']'") + } + + return &slice{start: start, end: end, step: step}, nil +} + +func (p *JSONPath) checkSafeInteger(i int64, literal string) error { + if i > MaxSafeFloat || i < -MaxSafeFloat { + return p.parseFailure(&p.tokens[p.current], "outside bounds for safe integers") + } + if literal == "-0" { + return p.parseFailure(&p.tokens[p.current], "-0 unexpected") + } + return nil +} + +func (p *JSONPath) parseFilterSelector() (*selector, error) { + + if p.tokens[p.current].Token != token.FILTER { + return nil, p.parseFailure(&p.tokens[p.current], "expected '?'") + } + p.current++ + + expr, err := p.parseLogicalOrExpr() + if err != nil { + return nil, err + } + + return &selector{kind: selectorSubKindFilter, filter: &filterSelector{expr}}, nil +} + +func (p *JSONPath) parseLogicalOrExpr() (*logicalOrExpr, error) { + var expr logicalOrExpr + + for { + andExpr, err := p.parseLogicalAndExpr() + if err != nil { + return nil, err + } + expr.expressions = append(expr.expressions, andExpr) + + if !p.next(token.OR) { + break + } + p.current++ + } + + return &expr, nil +} + +func (p *JSONPath) parseLogicalAndExpr() (*logicalAndExpr, error) { + var expr logicalAndExpr + + for { + basicExpr, err := p.parseBasicExpr() + if err != nil { + return nil, err + } + expr.expressions = append(expr.expressions, basicExpr) + + if !p.next(token.AND) { + break + } + p.current++ + } + + return &expr, nil +} + +func (p *JSONPath) parseBasicExpr() (*basicExpr, error) { + //basic-expr = paren-expr / + // comparison-expr / + // test-expr + + switch p.tokens[p.current].Token { + case token.NOT: + p.current++ + expr, err := p.parseLogicalOrExpr() + if err != nil { + return nil, err + } + // Inspect if the expr is topped by a parenExpr -- if so we can simplify + if len(expr.expressions) == 1 && len(expr.expressions[0].expressions) == 1 && expr.expressions[0].expressions[0].parenExpr != nil { + child := expr.expressions[0].expressions[0].parenExpr + child.not = !child.not + return &basicExpr{parenExpr: child}, nil + } + return &basicExpr{parenExpr: &parenExpr{not: true, expr: expr}}, nil + case token.PAREN_LEFT: + p.current++ + expr, err := p.parseLogicalOrExpr() + if err != nil { + return nil, err + } + if p.tokens[p.current].Token != token.PAREN_RIGHT { + return nil, p.parseFailure(&p.tokens[p.current], "expected ')'") + } + p.current++ + return &basicExpr{parenExpr: &parenExpr{not: false, expr: expr}}, nil + } + prevCurrent := p.current + comparisonExpr, comparisonErr := p.parseComparisonExpr() + if comparisonErr == nil { + return &basicExpr{comparisonExpr: comparisonExpr}, nil + } + p.current = prevCurrent + testExpr, testErr := p.parseTestExpr() + if testErr == nil { + return &basicExpr{testExpr: testExpr}, nil + } + p.current = prevCurrent + return nil, p.parseFailure(&p.tokens[p.current], fmt.Sprintf("could not parse query: expected either testExpr [err: %s] or comparisonExpr: [err: %s]", testErr.Error(), comparisonErr.Error())) +} + +func (p *JSONPath) parseComparisonExpr() (*comparisonExpr, error) { + left, err := p.parseComparable() + if err != nil { + return nil, err + } + + if !p.isComparisonOperator(p.tokens[p.current].Token) { + return nil, p.parseFailure(&p.tokens[p.current], "expected comparison operator") + } + operator := p.tokens[p.current].Token + var op comparisonOperator + switch operator { + case token.EQ: + op = equalTo + case token.NE: + op = notEqualTo + case token.LT: + op = lessThan + case token.LE: + op = lessThanEqualTo + case token.GT: + op = greaterThan + case token.GE: + op = greaterThanEqualTo + default: + return nil, p.parseFailure(&p.tokens[p.current], "expected comparison operator") + } + p.current++ + + right, err := p.parseComparable() + if err != nil { + return nil, err + } + + return &comparisonExpr{left: left, op: op, right: right}, nil +} + +func (p *JSONPath) parseComparable() (*comparable, error) { + // comparable = literal / + // singular-query / ; singular query value + // function-expr ; ValueType + if literal, err := p.parseLiteral(); err == nil { + return &comparable{literal: literal}, nil + } + if funcExpr, err := p.parseFunctionExpr(); err == nil { + if funcExpr.funcType == functionTypeMatch { + return nil, p.parseFailure(&p.tokens[p.current], "match result cannot be compared") + } else if funcExpr.funcType == functionTypeSearch { + return nil, p.parseFailure(&p.tokens[p.current], "search result cannot be compared") + } + return &comparable{functionExpr: funcExpr}, nil + } + switch p.tokens[p.current].Token { + case token.ROOT: + p.current++ + query, err := p.parseSingleQuery() + if err != nil { + return nil, err + } + return &comparable{singularQuery: &singularQuery{absQuery: &absQuery{segments: query.segments}}}, nil + case token.CURRENT: + p.current++ + query, err := p.parseSingleQuery() + if err != nil { + return nil, err + } + return &comparable{singularQuery: &singularQuery{relQuery: &relQuery{segments: query.segments}}}, nil + default: + return nil, p.parseFailure(&p.tokens[p.current], "expected literal or query") + } +} + +func (p *JSONPath) parseQuery() (*jsonPathAST, error) { + var query jsonPathAST + p.mode = append(p.mode, modeNormal) + + for p.current < len(p.tokens) { + prior := p.current + segment, err := p.parseSegment() + if err != nil { + p.current = prior + break + } + query.segments = append(query.segments, segment) + } + p.mode = p.mode[:len(p.mode)-1] + return &query, nil +} + +func (p *JSONPath) parseTestExpr() (*testExpr, error) { + //test-expr = [logical-not-op S] + // (filter-query / ; existence/non-existence + // function-expr) ; LogicalType or NodesType + //filter-query = rel-query / jsonpath-query + //rel-query = current-node-identifier segments + //current-node-identifier = "@" + not := false + if p.tokens[p.current].Token == token.NOT { + not = true + p.current++ + } + switch p.tokens[p.current].Token { + case token.CURRENT: + p.current++ + query, err := p.parseQuery() + if err != nil { + return nil, err + } + return &testExpr{filterQuery: &filterQuery{relQuery: &relQuery{segments: query.segments}}, not: not}, nil + case token.ROOT: + p.current++ + query, err := p.parseQuery() + if err != nil { + return nil, err + } + return &testExpr{filterQuery: &filterQuery{jsonPathQuery: &jsonPathAST{segments: query.segments}}, not: not}, nil + default: + funcExpr, err := p.parseFunctionExpr() + if err != nil { + return nil, err + } + if funcExpr.funcType == functionTypeCount { + return nil, p.parseFailure(&p.tokens[p.current], "count function must be compared") + } + if funcExpr.funcType == functionTypeLength { + return nil, p.parseFailure(&p.tokens[p.current], "length function must be compared") + } + if funcExpr.funcType == functionTypeValue { + return nil, p.parseFailure(&p.tokens[p.current], "length function must be compared") + } + return &testExpr{functionExpr: funcExpr, not: not}, nil + } + + return nil, p.parseFailure(&p.tokens[p.current], "unexpected token when parsing test expression") +} + +func (p *JSONPath) parseFunctionExpr() (*functionExpr, error) { + functionName := p.tokens[p.current].Literal + if p.current+1 >= len(p.tokens) || p.tokens[p.current+1].Token != token.PAREN_LEFT { + return nil, p.parseFailure(&p.tokens[p.current], "expected '(' after function") + } + p.current += 2 + args := []*functionArgument{} + switch functionTypeMap[functionName] { + case functionTypeLength: + arg, err := p.parseFunctionArgument(true) + if err != nil { + return nil, err + } + args = append(args, arg) + case functionTypeCount: + arg, err := p.parseFunctionArgument(false) + if err != nil { + return nil, err + } + if arg.literal != nil && arg.literal.node == nil { + return nil, p.parseFailure(&p.tokens[p.current], "count function only supports containers") + } + args = append(args, arg) + case functionTypeValue: + arg, err := p.parseFunctionArgument(false) + if err != nil { + return nil, err + } + args = append(args, arg) + case functionTypeMatch: + fallthrough + case functionTypeSearch: + arg, err := p.parseFunctionArgument(false) + if err != nil { + return nil, err + } + args = append(args, arg) + if p.tokens[p.current].Token != token.COMMA { + return nil, p.parseFailure(&p.tokens[p.current], "expected ','") + } + p.current++ + arg, err = p.parseFunctionArgument(false) + if err != nil { + return nil, err + } + args = append(args, arg) + } + if p.tokens[p.current].Token != token.PAREN_RIGHT { + return nil, p.parseFailure(&p.tokens[p.current], "expected ')'") + } + p.current++ + return &functionExpr{funcType: functionTypeMap[functionName], args: args}, nil +} + +func (p *JSONPath) parseSingleQuery() (*jsonPathAST, error) { + var query jsonPathAST + for p.current < len(p.tokens) { + try := p.current + p.mode = append(p.mode, modeSingular) + segment, err := p.parseSegment() + if err != nil { + // rollback + p.mode = p.mode[:len(p.mode)-1] + p.current = try + break + } + p.mode = p.mode[:len(p.mode)-1] + query.segments = append(query.segments, segment) + } + //if len(query.segments) == 0 { + // return nil, p.parseFailure(p.tokens[p.current], "expected at least one segment") + //} + return &query, nil +} + +func (p *JSONPath) parseFunctionArgument(single bool) (*functionArgument, error) { + //function-argument = literal / + // filter-query / ; (includes singular-query) + // logical-expr / + // function-expr + + if lit, err := p.parseLiteral(); err == nil { + return &functionArgument{literal: lit}, nil + } + switch p.tokens[p.current].Token { + case token.CURRENT: + p.current++ + var query *jsonPathAST + var err error + if single { + query, err = p.parseSingleQuery() + } else { + query, err = p.parseQuery() + } + if err != nil { + return nil, err + } + return &functionArgument{filterQuery: &filterQuery{relQuery: &relQuery{segments: query.segments}}}, nil + case token.ROOT: + p.current++ + var query *jsonPathAST + var err error + if single { + query, err = p.parseSingleQuery() + } else { + query, err = p.parseQuery() + } + if err != nil { + return nil, err + } + return &functionArgument{filterQuery: &filterQuery{jsonPathQuery: &jsonPathAST{segments: query.segments}}}, nil + } + if expr, err := p.parseLogicalOrExpr(); err == nil { + return &functionArgument{logicalExpr: expr}, nil + } + if funcExpr, err := p.parseFunctionExpr(); err == nil { + return &functionArgument{functionExpr: funcExpr}, nil + } + + return nil, p.parseFailure(&p.tokens[p.current], "unexpected token for function argument") +} + +func (p *JSONPath) parseLiteral() (*literal, error) { + switch p.tokens[p.current].Token { + case token.STRING_LITERAL: + lit := p.tokens[p.current].Literal + p.current++ + return &literal{string: &lit}, nil + case token.INTEGER: + lit := p.tokens[p.current].Literal + p.current++ + i, err := strconv.Atoi(lit) + if err != nil { + return nil, p.parseFailure(&p.tokens[p.current], "expected integer") + } + return &literal{integer: &i}, nil + case token.FLOAT: + lit := p.tokens[p.current].Literal + p.current++ + f, err := strconv.ParseFloat(lit, 64) + if err != nil { + return nil, p.parseFailure(&p.tokens[p.current], "expected float") + } + return &literal{float64: &f}, nil + case token.TRUE: + p.current++ + res := true + return &literal{bool: &res}, nil + case token.FALSE: + p.current++ + res := false + return &literal{bool: &res}, nil + case token.NULL: + p.current++ + res := true + return &literal{null: &res}, nil + } + return nil, p.parseFailure(&p.tokens[p.current], "expected literal") +} + +type jsonPathAST struct { + // "$" + segments []*segment +} + +func (q jsonPathAST) ToString() string { + b := strings.Builder{} + b.WriteString("$") + for _, seg := range q.segments { + b.WriteString(seg.ToString()) + } + return b.String() +} diff --git a/vendor/github.com/speakeasy-api/jsonpath/pkg/jsonpath/segment.go b/vendor/github.com/speakeasy-api/jsonpath/pkg/jsonpath/segment.go new file mode 100644 index 0000000..cb6233f --- /dev/null +++ b/vendor/github.com/speakeasy-api/jsonpath/pkg/jsonpath/segment.go @@ -0,0 +1,83 @@ +package jsonpath + +import ( + "gopkg.in/yaml.v3" + "strings" +) + +type segmentKind int + +const ( + segmentKindChild segmentKind = iota // . + segmentKindDescendant // .. + segmentKindProperyName // ~ (extension only) +) + +type segment struct { + kind segmentKind + child *innerSegment + descendant *innerSegment +} + +type segmentSubKind int + +const ( + segmentDotWildcard segmentSubKind = iota // .* + segmentDotMemberName // .property + segmentLongHand // [ selector[] ] +) + +func (s segment) ToString() string { + switch s.kind { + case segmentKindChild: + if s.child.kind != segmentLongHand { + return "." + s.child.ToString() + } else { + return s.child.ToString() + } + case segmentKindDescendant: + return ".." + s.descendant.ToString() + case segmentKindProperyName: + return "~" + } + panic("unknown segment kind") +} + +type innerSegment struct { + kind segmentSubKind + dotName string + selectors []*selector +} + +func (s innerSegment) ToString() string { + builder := strings.Builder{} + switch s.kind { + case segmentDotWildcard: + builder.WriteString("*") + break + case segmentDotMemberName: + builder.WriteString(s.dotName) + break + case segmentLongHand: + builder.WriteString("[") + for i, selector := range s.selectors { + builder.WriteString(selector.ToString()) + if i < len(s.selectors)-1 { + builder.WriteString(", ") + } + } + builder.WriteString("]") + break + default: + panic("unknown child segment kind") + } + return builder.String() +} + +func descend(value *yaml.Node, root *yaml.Node) []*yaml.Node { + result := []*yaml.Node{value} + for _, child := range value.Content { + result = append(result, descend(child, root)...) + } + return result +} diff --git a/vendor/github.com/speakeasy-api/jsonpath/pkg/jsonpath/selector.go b/vendor/github.com/speakeasy-api/jsonpath/pkg/jsonpath/selector.go new file mode 100644 index 0000000..f71cb47 --- /dev/null +++ b/vendor/github.com/speakeasy-api/jsonpath/pkg/jsonpath/selector.go @@ -0,0 +1,63 @@ +package jsonpath + +import ( + "fmt" + "strconv" + "strings" +) + +type selectorSubKind int + +const ( + selectorSubKindWildcard selectorSubKind = iota + selectorSubKindName + selectorSubKindArraySlice + selectorSubKindArrayIndex + selectorSubKindFilter +) + +type slice struct { + start *int64 + end *int64 + step *int64 +} + +type selector struct { + kind selectorSubKind + name string + index int64 + slice *slice + filter *filterSelector +} + +func (s selector) ToString() string { + switch s.kind { + case selectorSubKindName: + return "'" + escapeString(s.name) + "'" + case selectorSubKindArrayIndex: + // int to string + return strconv.FormatInt(s.index, 10) + case selectorSubKindFilter: + return "?" + s.filter.ToString() + case selectorSubKindWildcard: + return "*" + case selectorSubKindArraySlice: + builder := strings.Builder{} + if s.slice.start != nil { + builder.WriteString(strconv.FormatInt(*s.slice.start, 10)) + } + builder.WriteString(":") + if s.slice.end != nil { + builder.WriteString(strconv.FormatInt(*s.slice.end, 10)) + } + + if s.slice.step != nil { + builder.WriteString(":") + builder.WriteString(strconv.FormatInt(*s.slice.step, 10)) + } + return builder.String() + default: + panic(fmt.Sprintf("unimplemented selector kind: %v", s.kind)) + } + return "" +} diff --git a/vendor/github.com/speakeasy-api/jsonpath/pkg/jsonpath/token/token.go b/vendor/github.com/speakeasy-api/jsonpath/pkg/jsonpath/token/token.go new file mode 100644 index 0000000..6cf1654 --- /dev/null +++ b/vendor/github.com/speakeasy-api/jsonpath/pkg/jsonpath/token/token.go @@ -0,0 +1,776 @@ +package token + +import ( + "fmt" + "github.com/speakeasy-api/jsonpath/pkg/jsonpath/config" + "strconv" + "strings" +) + +// ***************************************************************************** +// The Tokenizer is responsible for tokenizing the jsonpath expression. This means +// * removing whitespace +// * scanning strings +// * and detecting illegal characters +// ***************************************************************************** + +// Token represents a lexical token in a JSONPath expression. +type Token int + +// We are allowed the following tokens + +//jsonpath-query = root-identifier segments +//segments = *(segment) +//root-identifier = "$" +//selector = name-selector / +// wildcard-selector / +// slice-selector / +// index-selector / +// filter-selector +//name-selector = string-literal +//wildcard-selector = "*" +//index-selector = int ; decimal integer +// +//int = "0" / +// (["-"] DIGIT1 *DIGIT) ; - optional +//DIGIT1 = %x31-39 ; 1-9 non-zero digit +//slice-selector = [start] ":" [end] [":" [step]] +// +//start = int ; included in selection +//end = int ; not included in selection +//step = int ; default: 1 +//filter-selector = "?" logical-expr +//logical-expr = logical-or-expr +//logical-or-expr = logical-and-expr *("||" logical-and-expr) +// ; disjunction +// ; binds less tightly than conjunction +//logical-and-expr = basic-expr *("&&" basic-expr) +// ; conjunction +// ; binds more tightly than disjunction +// +//basic-expr = paren-expr / +// comparison-expr / +// test-expr +// +//paren-expr = [logical-not-op] "(" logical-expr ")" +// ; parenthesized expression +//logical-not-op = "!" ; logical NOT operator +//test-expr = [logical-not-op S] +// (filter-query / ; existence/non-existence +// function-expr) ; LogicalType or NodesType +//filter-query = rel-query / jsonpath-query +//rel-query = current-node-identifier segments +//current-node-identifier = "@" +//comparison-expr = comparable comparison-op comparable +//literal = number / string-literal / +// true / false / null +//comparable = literal / +// singular-query / ; singular query value +// function-expr ; ValueType +//comparison-op = "==" / "!=" / +// "<=" / ">=" / +// "<" / ">" +// +//singular-query = rel-singular-query / abs-singular-query +//rel-singular-query = current-node-identifier singular-query-segments +//abs-singular-query = root-identifier singular-query-segments +//singular-query-segments = *(S (name-segment / index-segment)) +//name-segment = ("[" name-selector "]") / +// ("." member-name-shorthand) +//index-segment = "[" index-selector "]" +//number = (int / "-0") [ frac ] [ exp ] ; decimal number +//frac = "." 1*DIGIT ; decimal fraction +//exp = "e" [ "-" / "+" ] 1*DIGIT ; decimal exponent +//true = %x74.72.75.65 ; true +//false = %x66.61.6c.73.65 ; false +//null = %x6e.75.6c.6c ; null +//function-name = function-name-first *function-name-char +//function-name-first = LCALPHA +//function-name-char = function-name-first / "_" / DIGIT +//LCALPHA = %x61-7A ; "a".."z" +// +//function-expr = function-name "(" [function-argument +// *(S "," function-argument)] ")" +//function-argument = literal / +// filter-query / ; (includes singular-query) +// logical-expr / +// function-expr +//segment = child-segment / descendant-segment +//child-segment = bracketed-selection / +// ("." +// (wildcard-selector / +// member-name-shorthand)) +// +//bracketed-selection = "[" selector *(S "," selector) "]" +// +//member-name-shorthand = name-first *name-char +//name-first = ALPHA / +// "_" / +// %x80-D7FF / +// ; skip surrogate code points +// %xE000-10FFFF +//name-char = name-first / DIGIT +// +//DIGIT = %x30-39 ; 0-9 +//ALPHA = %x41-5A / %x61-7A ; A-Z / a-z +//descendant-segment = ".." (bracketed-selection / +// wildcard-selector / +// member-name-shorthand) +// +// Figure 2: Collected ABNF of JSONPath Queries +// +//Figure 3 contains the collected ABNF grammar that defines the syntax +//of a JSONPath Normalized Path while also using the rules root- +//identifier, ESC, DIGIT, and DIGIT1 from Figure 2. +// +//normalized-path = root-identifier *(normal-index-segment) +//normal-index-segment = "[" normal-selector "]" +//normal-selector = normal-name-selector / normal-index-selector +//normal-name-selector = %x27 *normal-single-quoted %x27 ; 'string' +//normal-single-quoted = normal-unescaped / +// ESC normal-escapable +//normal-unescaped = ; omit %x0-1F control codes +// %x20-26 / +// ; omit 0x27 ' +// %x28-5B / +// ; omit 0x5C \ +// %x5D-D7FF / +// ; skip surrogate code points +// %xE000-10FFFF +// +//normal-escapable = %x62 / ; b BS backspace U+0008 +// %x66 / ; f FF form feed U+000C +// %x6E / ; n LF line feed U+000A +// %x72 / ; r CR carriage return U+000D +// %x74 / ; t HT horizontal tab U+0009 +// "'" / ; ' apostrophe U+0027 +// "\" / ; \ backslash (reverse solidus) U+005C +// (%x75 normal-hexchar) +// ; certain values u00xx U+00XX +//normal-hexchar = "0" "0" +// ( +// ("0" %x30-37) / ; "00"-"07" +// ; omit U+0008-U+000A BS HT LF +// ("0" %x62) / ; "0b" +// ; omit U+000C-U+000D FF CR +// ("0" %x65-66) / ; "0e"-"0f" +// ("1" normal-HEXDIG) +// ) +//normal-HEXDIG = DIGIT / %x61-66 ; "0"-"9", "a"-"f" +//normal-index-selector = "0" / (DIGIT1 *DIGIT) +// ; non-negative decimal integer + +// The list of tokens. +const ( + ILLEGAL Token = iota + STRING + INTEGER + FLOAT + STRING_LITERAL + TRUE + FALSE + NULL + ROOT + CURRENT + WILDCARD + PROPERTY_NAME + RECURSIVE + CHILD + ARRAY_SLICE + FILTER + PAREN_LEFT + PAREN_RIGHT + BRACKET_LEFT + BRACKET_RIGHT + COMMA + TILDE + AND + OR + NOT + EQ + NE + GT + GE + LT + LE + MATCHES + FUNCTION +) + +var SimpleTokens = [...]Token{ + STRING, + INTEGER, + STRING_LITERAL, + CHILD, + BRACKET_LEFT, + BRACKET_RIGHT, + ROOT, +} + +var tokens = [...]string{ + ILLEGAL: "ILLEGAL", + STRING: "STRING", + INTEGER: "INTEGER", + FLOAT: "FLOAT", + STRING_LITERAL: "STRING_LITERAL", + TRUE: "TRUE", + FALSE: "FALSE", + NULL: "NULL", + // root node identifier (Section 2.2) + ROOT: "$", + // current node identifier (Section 2.3.5) + // (valid only within filter selectors) + CURRENT: "@", + WILDCARD: "*", + RECURSIVE: "..", + CHILD: ".", + // start:end:step array slice operator (Section 2.3.4) + ARRAY_SLICE: ":", + // filter selector (Section 2.3.5): selects + // particular children using a logical + // expression + FILTER: "?", + PAREN_LEFT: "(", + PAREN_RIGHT: ")", + BRACKET_LEFT: "[", + BRACKET_RIGHT: "]", + COMMA: ",", + TILDE: "~", + AND: "&&", + OR: "||", + NOT: "!", + EQ: "==", + NE: "!=", + GT: ">", + GE: ">=", + LT: "<", + LE: "<=", + MATCHES: "=~", + FUNCTION: "FUNCTION", +} + +// String returns the string representation of the token. +func (tok Token) String() string { + if tok >= 0 && tok < Token(len(tokens)) { + return tokens[tok] + } + return "token(" + strconv.Itoa(int(tok)) + ")" +} + +func (tok Tokens) IsSimple() bool { + if len(tok) == 0 { + return false + } + if tok[0].Token != ROOT { + return false + } + for _, token := range tok { + isSimple := false + for _, simpleToken := range SimpleTokens { + if token.Token == simpleToken { + isSimple = true + } + } + if !isSimple { + return false + } + } + return true +} + +// When there's an error in the tokenizer, this helps represent it. +func (t Tokenizer) ErrorString(target *TokenInfo, msg string) string { + var errorBuilder strings.Builder + + var token TokenInfo + if target == nil { + // grab last token (as value) + token = t.tokens[len(t.tokens)-1] + // set column to +1 + token.Column++ + target = &token + } + + // Write the error message with line and column information + errorBuilder.WriteString(fmt.Sprintf("Error at line %d, column %d: %s\n", target.Line, target.Column, msg)) + + // Find the start and end positions of the line containing the target token + lineStart := 0 + lineEnd := len(t.input) + for i := target.Line - 1; i > 0; i-- { + if pos := strings.LastIndexByte(t.input[:lineStart], '\n'); pos != -1 { + lineStart = pos + 1 + break + } + } + if pos := strings.IndexByte(t.input[lineStart:], '\n'); pos != -1 { + lineEnd = lineStart + pos + } + + // Extract the line containing the target token + line := t.input[lineStart:lineEnd] + errorBuilder.WriteString(line) + errorBuilder.WriteString("\n") + + // Calculate the number of spaces before the target token + spaces := strings.Repeat(" ", target.Column) + + // Write the caret symbol pointing to the target token + errorBuilder.WriteString(spaces) + dots := "" + if target.Len > 0 { + dots = strings.Repeat(".", target.Len-1) + } + errorBuilder.WriteString("^" + dots + "\n") + + return errorBuilder.String() +} + +// When there's an error +func (t Tokenizer) ErrorTokenString(target *TokenInfo, msg string) string { + var errorBuilder strings.Builder + var token TokenInfo + if target == nil { + // grab last token (as value) + token = t.tokens[len(t.tokens)-1] + // set column to +1 + token.Column++ + target = &token + } + // Write the error message with line and column information + errorBuilder.WriteString(t.ErrorString(target, msg)) + + // Find the start and end positions of the line containing the target token + lineStart := 0 + lineEnd := len(t.input) + for i := target.Line - 1; i > 0; i-- { + if pos := strings.LastIndexByte(t.input[:lineStart], '\n'); pos != -1 { + lineStart = pos + 1 + break + } + } + if pos := strings.IndexByte(t.input[lineStart:], '\n'); pos != -1 { + lineEnd = lineStart + pos + } + + // Extract the line containing the target token + line := t.input[lineStart:lineEnd] + + // Calculate the number of spaces before the target token + for _, token := range t.tokens { + errorBuilder.WriteString(line) + errorBuilder.WriteString("\n") + spaces := strings.Repeat(" ", token.Column) + dots := "" + if token.Len > 0 { + dots = strings.Repeat(".", token.Len-1) + } + errorBuilder.WriteString(spaces) + errorBuilder.WriteString(fmt.Sprintf("^%s %s\n", dots, tokens[token.Token])) + } + + return errorBuilder.String() +} + +// TokenInfo represents a token and its associated information. +type TokenInfo struct { + Token Token + Line int + Column int + Literal string + Len int +} + +// Tokens represents the list of tokens +type Tokens []TokenInfo + +// Tokenizer represents a JSONPath tokenizer. +type Tokenizer struct { + input string + pos int + line int + column int + tokens []TokenInfo + stack []Token + illegalWhitespace bool + config config.Config +} + +// NewTokenizer creates a new JSONPath tokenizer for the given input string. +func NewTokenizer(input string, opts ...config.Option) *Tokenizer { + cfg := config.New(opts...) + return &Tokenizer{ + input: input, + config: cfg, + line: 1, + stack: make([]Token, 0), + } +} + +// Tokenize tokenizes the input string and returns a slice of TokenInfo. +func (t *Tokenizer) Tokenize() Tokens { + for t.pos < len(t.input) { + if !t.illegalWhitespace { + t.skipWhitespace() + } + if t.pos >= len(t.input) { + break + } + + switch ch := t.input[t.pos]; { + case ch == '$': + t.addToken(ROOT, 1, "") + case ch == '@': + t.addToken(CURRENT, 1, "") + case ch == '*': + t.addToken(WILDCARD, 1, "") + case ch == '~': + if t.config.PropertyNameEnabled() { + t.addToken(PROPERTY_NAME, 1, "") + } else { + t.addToken(ILLEGAL, 1, "invalid property name token without config.PropertyNameExtension set to true") + } + case ch == '.': + if t.peek() == '.' { + t.addToken(RECURSIVE, 2, "") + t.pos++ + t.column++ + t.illegalWhitespace = true + } else { + t.addToken(CHILD, 1, "") + t.illegalWhitespace = true + } + case ch == ',': + t.addToken(COMMA, 1, "") + case ch == ':': + t.addToken(ARRAY_SLICE, 1, "") + case ch == '?': + t.addToken(FILTER, 1, "") + case ch == '(': + t.addToken(PAREN_LEFT, 1, "") + t.stack = append(t.stack, PAREN_LEFT) + case ch == ')': + t.addToken(PAREN_RIGHT, 1, "") + if len(t.stack) > 0 && t.stack[len(t.stack)-1] == PAREN_LEFT { + t.stack = t.stack[:len(t.stack)-1] + } else { + t.addToken(ILLEGAL, 1, "unmatched closing parenthesis") + } + case ch == '[': + t.addToken(BRACKET_LEFT, 1, "") + t.stack = append(t.stack, BRACKET_LEFT) + case ch == ']': + if len(t.stack) > 0 && t.stack[len(t.stack)-1] == BRACKET_LEFT { + t.addToken(BRACKET_RIGHT, 1, "") + t.stack = t.stack[:len(t.stack)-1] + } else { + t.addToken(ILLEGAL, 1, "unmatched closing bracket") + } + case ch == '&': + if t.peek() == '&' { + t.addToken(AND, 2, "") + t.pos++ + t.column++ + } else { + t.addToken(ILLEGAL, 1, "invalid token") + } + case ch == '|': + if t.peek() == '|' { + t.addToken(OR, 2, "") + t.pos++ + t.column++ + } else { + t.addToken(ILLEGAL, 1, "invalid token") + } + case ch == '!': + if t.peek() == '=' { + t.addToken(NE, 2, "") + t.pos++ + t.column++ + } else { + t.addToken(NOT, 1, "") + } + case ch == '=': + if t.peek() == '=' { + t.addToken(EQ, 2, "") + t.pos++ + t.column++ + } else if t.peek() == '~' { + t.addToken(MATCHES, 2, "") + t.pos++ + t.column++ + } else { + t.addToken(ILLEGAL, 1, "invalid token") + } + case ch == '>': + if t.peek() == '=' { + t.addToken(GE, 2, "") + t.pos++ + t.column++ + } else { + t.addToken(GT, 1, "") + } + case ch == '<': + if t.peek() == '=' { + t.addToken(LE, 2, "") + t.pos++ + t.column++ + } else { + t.addToken(LT, 1, "") + } + case ch == '"' || ch == '\'': + t.scanString(rune(ch)) + case ch == '-' && isDigit(t.peek()): + fallthrough + case isDigit(ch): + t.scanNumber() + case isLiteralChar(ch): + t.scanLiteral() + default: + t.addToken(ILLEGAL, 1, string(ch)) + } + t.pos++ + t.column++ + } + + if len(t.stack) > 0 { + t.addToken(ILLEGAL, 1, fmt.Sprintf("unmatched %s", t.stack[len(t.stack)-1].String())) + } + return t.tokens +} + +func (t *Tokenizer) addToken(token Token, len int, literal string) { + t.tokens = append(t.tokens, TokenInfo{ + Token: token, + Line: t.line, + Column: t.column, + Len: len, + Literal: literal, + }) + t.illegalWhitespace = false +} + +func (t *Tokenizer) scanString(quote rune) { + start := t.pos + 1 + var literal strings.Builder +illegal: + for i := start; i < len(t.input); i++ { + b := literal.String() + _ = b + if t.input[i] == byte(quote) { + t.addToken(STRING_LITERAL, len(t.input[start:i])+2, literal.String()) + t.pos = i + t.column += i - start + 1 + return + } + if t.input[i] == '\\' { + i++ + if i >= len(t.input) { + t.addToken(ILLEGAL, len(t.input[start:]), literal.String()) + t.pos = len(t.input) - 1 + t.column = len(t.input) - 1 + return + } + switch t.input[i] { + case 'b': + literal.WriteByte('\b') + case 'f': + literal.WriteByte('\f') + case 'n': + literal.WriteByte('\n') + case 'r': + literal.WriteByte('\n') + case 't': + literal.WriteByte('\t') + case '\'': + if quote != '\'' { + // don't escape it, when we're not in a single quoted string + break illegal + } else { + literal.WriteByte(t.input[i]) + } + case '"': + if quote != '"' { + // don't escape it, when we're not in a single quoted string + break illegal + } else { + literal.WriteByte(t.input[i]) + } + case '\\', '/': + literal.WriteByte(t.input[i]) + default: + break illegal + } + } else { + literal.WriteByte(t.input[i]) + } + } + t.addToken(ILLEGAL, len(t.input[start:]), literal.String()) + t.pos = len(t.input) - 1 + t.column = len(t.input) - 1 +} + +func (t *Tokenizer) scanNumber() { + start := t.pos + tokenType := INTEGER + dotSeen := false + exponentSeen := false + + for i := start; i < len(t.input); i++ { + if i == start && t.input[i] == '-' { + continue + } + + if t.input[i] == '.' { + if dotSeen || exponentSeen { + t.addToken(ILLEGAL, len(t.input[start:i]), t.input[start:i]) + t.pos = i + t.column += i - start + return + } + tokenType = FLOAT + dotSeen = true + continue + } + + if t.input[i] == 'e' || t.input[i] == 'E' { + if exponentSeen || (len(t.input) > 0 && t.input[i-1] == '.') { + t.addToken(ILLEGAL, len(t.input[start:i]), t.input[start:i]) + t.pos = i + t.column += i - start + return + } + tokenType = FLOAT + exponentSeen = true + if i+1 < len(t.input) && (t.input[i+1] == '+' || t.input[i+1] == '-') { + i++ + } + continue + } + + if !isDigit(t.input[i]) { + literal := t.input[start:i] + // check for legal numbers + _, err := strconv.ParseFloat(literal, 64) + if err != nil { + tokenType = ILLEGAL + } + // conformance spec + if len(literal) > 1 && literal[0] == '0' && !dotSeen { + // no leading zero + tokenType = ILLEGAL + } else if len(literal) > 2 && literal[0] == '-' && literal[1] == '0' && !dotSeen { + // no trailing dot + tokenType = ILLEGAL + } else if len(literal) > 0 && literal[len(literal)-1] == '.' { + // no trailing dot + tokenType = ILLEGAL + } else if literal[len(literal)-1] == 'e' || literal[len(literal)-1] == 'E' { + // no exponent + tokenType = ILLEGAL + } + + t.addToken(tokenType, len(literal), literal) + t.pos = i - 1 + t.column += i - start - 1 + return + } + } + + if exponentSeen && !isDigit(t.input[len(t.input)-1]) { + t.addToken(ILLEGAL, len(t.input[start:]), t.input[start:]) + t.pos = len(t.input) - 1 + t.column = len(t.input) - 1 + return + } + + literal := t.input[start:] + t.addToken(tokenType, len(literal), literal) + t.pos = len(t.input) - 1 + t.column = len(t.input) - 1 +} + +func (t *Tokenizer) scanLiteral() { + start := t.pos + for i := start; i < len(t.input); i++ { + if !isLiteralChar(t.input[i]) && !isDigit(t.input[i]) { + literal := t.input[start:i] + switch literal { + case "true": + t.addToken(TRUE, len(literal), literal) + case "false": + t.addToken(FALSE, len(literal), literal) + case "null": + t.addToken(NULL, len(literal), literal) + default: + if isFunctionName(literal) { + t.addToken(FUNCTION, len(literal), literal) + t.illegalWhitespace = true + } else { + t.addToken(STRING, len(literal), literal) + } + } + t.pos = i - 1 + t.column += i - start - 1 + return + } + } + literal := t.input[start:] + switch literal { + case "true": + t.addToken(TRUE, len(literal), literal) + case "false": + t.addToken(FALSE, len(literal), literal) + case "null": + t.addToken(NULL, len(literal), literal) + default: + t.addToken(STRING, len(literal), literal) + } + t.pos = len(t.input) - 1 + t.column = len(t.input) - 1 +} + +func isFunctionName(literal string) bool { + return literal == "length" || literal == "count" || literal == "match" || literal == "search" || literal == "value" +} + +func (t *Tokenizer) skipWhitespace() { + // S = *B ; optional blank space + // B = %x20 / ; Space + // %x09 / ; Horizontal tab + // %x0A / ; Line feed or New line + // %x0D ; Carriage return + for len(t.tokens) > 0 && t.pos+1 < len(t.input) { + ch := t.input[t.pos] + if ch == '\n' { + t.line++ + t.pos++ + t.column = 0 + } else if !isSpace(ch) { + break + } else { + t.pos++ + t.column++ + } + } +} + +func (t *Tokenizer) peek() byte { + if t.pos+1 < len(t.input) { + return t.input[t.pos+1] + } + return 0 +} + +func isDigit(ch byte) bool { + return '0' <= ch && ch <= '9' +} + +func isLiteralChar(ch byte) bool { + // allow unicode characters + return 'a' <= ch && ch <= 'z' || 'A' <= ch && ch <= 'Z' || ch == '_' || ch >= 0x80 +} + +func isSpace(ch byte) bool { + return ch == ' ' || ch == '\t' || ch == '\r' +} diff --git a/vendor/github.com/speakeasy-api/jsonpath/pkg/jsonpath/yaml_eval.go b/vendor/github.com/speakeasy-api/jsonpath/pkg/jsonpath/yaml_eval.go new file mode 100644 index 0000000..7291ea9 --- /dev/null +++ b/vendor/github.com/speakeasy-api/jsonpath/pkg/jsonpath/yaml_eval.go @@ -0,0 +1,278 @@ +package jsonpath + +import ( + "fmt" + "gopkg.in/yaml.v3" + "reflect" + "regexp" + "strconv" + "unicode/utf8" +) + +func (l literal) Equals(value literal) bool { + if l.integer != nil && value.integer != nil { + return *l.integer == *value.integer + } + if l.float64 != nil && value.float64 != nil { + return *l.float64 == *value.float64 + } + if l.integer != nil && value.float64 != nil { + return float64(*l.integer) == *value.float64 + } + if l.float64 != nil && value.integer != nil { + return *l.float64 == float64(*value.integer) + } + if l.string != nil && value.string != nil { + return *l.string == *value.string + } + if l.bool != nil && value.bool != nil { + return *l.bool == *value.bool + } + if l.null != nil && value.null != nil { + return *l.null == *value.null + } + if l.node != nil && value.node != nil { + return equalsNode(l.node, value.node) + } + if reflect.ValueOf(l).IsZero() && reflect.ValueOf(value).IsZero() { + return true + } + return false +} + +func equalsNode(a *yaml.Node, b *yaml.Node) bool { + // decode into interfaces, then compare + if a.Tag != b.Tag { + return false + } + switch a.Tag { + case "!!str": + return a.Value == b.Value + case "!!int": + return a.Value == b.Value + case "!!float": + return a.Value == b.Value + case "!!bool": + return a.Value == b.Value + case "!!null": + return a.Value == b.Value + case "!!seq": + if len(a.Content) != len(b.Content) { + return false + } + for i := 0; i < len(a.Content); i++ { + if !equalsNode(a.Content[i], b.Content[i]) { + return false + } + } + case "!!map": + if len(a.Content) != len(b.Content) { + return false + } + for i := 0; i < len(a.Content); i += 2 { + if !equalsNode(a.Content[i], b.Content[i]) { + return false + } + if !equalsNode(a.Content[i+1], b.Content[i+1]) { + return false + } + } + } + return true +} + +func (l literal) LessThan(value literal) bool { + if l.integer != nil && value.integer != nil { + return *l.integer < *value.integer + } + if l.float64 != nil && value.float64 != nil { + return *l.float64 < *value.float64 + } + if l.integer != nil && value.float64 != nil { + return float64(*l.integer) < *value.float64 + } + if l.float64 != nil && value.integer != nil { + return *l.float64 < float64(*value.integer) + } + if l.string != nil && value.string != nil { + return *l.string < *value.string + } + return false +} + +func (l literal) LessThanOrEqual(value literal) bool { + return l.LessThan(value) || l.Equals(value) +} + +func (c comparable) Evaluate(idx index, node *yaml.Node, root *yaml.Node) literal { + if c.literal != nil { + return *c.literal + } + if c.singularQuery != nil { + return c.singularQuery.Evaluate(idx, node, root) + } + if c.functionExpr != nil { + return c.functionExpr.Evaluate(idx, node, root) + } + return literal{} +} + +func (e functionExpr) length(idx index, node *yaml.Node, root *yaml.Node) literal { + args := e.args[0].Eval(idx, node, root) + if args.kind != functionArgTypeLiteral { + return literal{} + } + //* If the argument value is a string, the result is the number of + //Unicode scalar values in the string. + if args.literal != nil && args.literal.string != nil { + res := utf8.RuneCountInString(*args.literal.string) + return literal{integer: &res} + } + //* If the argument value is an array, the result is the number of + //elements in the array. + // + //* If the argument value is an object, the result is the number of + //members in the object. + // + //* For any other argument value, the result is the special result + //Nothing. + + if args.literal.node != nil { + switch args.literal.node.Kind { + case yaml.SequenceNode: + res := len(args.literal.node.Content) + return literal{integer: &res} + case yaml.MappingNode: + res := len(args.literal.node.Content) / 2 + return literal{integer: &res} + } + } + return literal{} +} + +func (e functionExpr) count(idx index, node *yaml.Node, root *yaml.Node) literal { + args := e.args[0].Eval(idx, node, root) + if args.kind == functionArgTypeNodes { + res := len(args.nodes) + return literal{integer: &res} + } + + res := 1 + return literal{integer: &res} +} + +func (e functionExpr) match(idx index, node *yaml.Node, root *yaml.Node) literal { + arg1 := e.args[0].Eval(idx, node, root) + arg2 := e.args[1].Eval(idx, node, root) + if arg1.kind != functionArgTypeLiteral || arg2.kind != functionArgTypeLiteral { + return literal{} + } + if arg1.literal.string == nil || arg2.literal.string == nil { + return literal{bool: &[]bool{false}[0]} + } + matched, _ := regexp.MatchString(fmt.Sprintf("^(%s)$", *arg2.literal.string), *arg1.literal.string) + return literal{bool: &matched} +} + +func (e functionExpr) search(idx index, node *yaml.Node, root *yaml.Node) literal { + arg1 := e.args[0].Eval(idx, node, root) + arg2 := e.args[1].Eval(idx, node, root) + if arg1.kind != functionArgTypeLiteral || arg2.kind != functionArgTypeLiteral { + return literal{} + } + if arg1.literal.string == nil || arg2.literal.string == nil { + return literal{bool: &[]bool{false}[0]} + } + matched, _ := regexp.MatchString(*arg2.literal.string, *arg1.literal.string) + return literal{bool: &matched} +} + +func (e functionExpr) value(idx index, node *yaml.Node, root *yaml.Node) literal { + // 2.4.8. value() Function Extension + // + //Parameters: + // 1. NodesType + // + //Result: ValueType + //Its only argument is an instance of NodesType (possibly taken from a + //filter-query, as in the example above). The result is an instance of + //ValueType. + // + //* If the argument contains a single node, the result is the value of + //the node. + // + //* If the argument is the empty nodelist or contains multiple nodes, + // the result is Nothing. + + nodesType := e.args[0].Eval(idx, node, root) + if nodesType.kind == functionArgTypeLiteral { + return *nodesType.literal + } else if nodesType.kind == functionArgTypeNodes && len(nodesType.nodes) == 1 { + return *nodesType.nodes[0] + } + return literal{} +} + +func nodeToLiteral(node *yaml.Node) literal { + switch node.Tag { + case "!!str": + return literal{string: &node.Value} + case "!!int": + i, _ := strconv.Atoi(node.Value) + return literal{integer: &i} + case "!!float": + f, _ := strconv.ParseFloat(node.Value, 64) + return literal{float64: &f} + case "!!bool": + b, _ := strconv.ParseBool(node.Value) + return literal{bool: &b} + case "!!null": + b := true + return literal{null: &b} + default: + return literal{node: node} + } +} + +func (e functionExpr) Evaluate(idx index, node *yaml.Node, root *yaml.Node) literal { + switch e.funcType { + case functionTypeLength: + return e.length(idx, node, root) + case functionTypeCount: + return e.count(idx, node, root) + case functionTypeMatch: + return e.match(idx, node, root) + case functionTypeSearch: + return e.search(idx, node, root) + case functionTypeValue: + return e.value(idx, node, root) + } + return literal{} +} + +func (q singularQuery) Evaluate(idx index, node *yaml.Node, root *yaml.Node) literal { + if q.relQuery != nil { + return q.relQuery.Evaluate(idx, node, root) + } + if q.absQuery != nil { + return q.absQuery.Evaluate(idx, node, root) + } + return literal{} +} + +func (q relQuery) Evaluate(idx index, node *yaml.Node, root *yaml.Node) literal { + result := q.Query(idx, node, root) + if len(result) == 1 { + return nodeToLiteral(result[0]) + } + return literal{} + +} + +func (q absQuery) Evaluate(idx index, node *yaml.Node, root *yaml.Node) literal { + result := q.Query(idx, root, root) + if len(result) == 1 { + return nodeToLiteral(result[0]) + } + return literal{} +} diff --git a/vendor/github.com/speakeasy-api/jsonpath/pkg/jsonpath/yaml_query.go b/vendor/github.com/speakeasy-api/jsonpath/pkg/jsonpath/yaml_query.go new file mode 100644 index 0000000..44b55a3 --- /dev/null +++ b/vendor/github.com/speakeasy-api/jsonpath/pkg/jsonpath/yaml_query.go @@ -0,0 +1,393 @@ +package jsonpath + +import ( + "gopkg.in/yaml.v3" +) + +type Evaluator interface { + Query(current *yaml.Node, root *yaml.Node) []*yaml.Node +} + +type index interface { + setPropertyKey(key *yaml.Node, value *yaml.Node) + getPropertyKey(key *yaml.Node) *yaml.Node +} + +type _index struct { + propertyKeys map[*yaml.Node]*yaml.Node +} + +func (i *_index) setPropertyKey(key *yaml.Node, value *yaml.Node) { + if i != nil && i.propertyKeys != nil { + i.propertyKeys[key] = value + } +} + +func (i *_index) getPropertyKey(key *yaml.Node) *yaml.Node { + if i != nil { + return i.propertyKeys[key] + } + return nil +} + +// jsonPathAST can be Evaluated +var _ Evaluator = jsonPathAST{} + +func (q jsonPathAST) Query(current *yaml.Node, root *yaml.Node) []*yaml.Node { + idx := _index{ + propertyKeys: map[*yaml.Node]*yaml.Node{}, + } + result := make([]*yaml.Node, 0) + // If the top level node is a documentnode, unwrap it + if root.Kind == yaml.DocumentNode && len(root.Content) == 1 { + root = root.Content[0] + } + result = append(result, root) + + for _, segment := range q.segments { + newValue := []*yaml.Node{} + for _, value := range result { + newValue = append(newValue, segment.Query(&idx, value, root)...) + } + result = newValue + } + return result +} + +func (s segment) Query(idx index, value *yaml.Node, root *yaml.Node) []*yaml.Node { + switch s.kind { + case segmentKindChild: + return s.child.Query(idx, value, root) + case segmentKindDescendant: + // run the inner segment against this node + var result = []*yaml.Node{} + children := descend(value, root) + for _, child := range children { + result = append(result, s.descendant.Query(idx, child, root)...) + } + // make children unique by pointer value + result = unique(result) + return result + case segmentKindProperyName: + found := idx.getPropertyKey(value) + if found != nil { + return []*yaml.Node{found} + } + return []*yaml.Node{} + } + panic("no segment type") +} + +func unique(nodes []*yaml.Node) []*yaml.Node { + // stably returns a new slice containing only the unique elements from nodes + res := make([]*yaml.Node, 0) + seen := make(map[*yaml.Node]bool) + for _, node := range nodes { + if _, ok := seen[node]; !ok { + res = append(res, node) + seen[node] = true + } + } + return res +} + +func (s innerSegment) Query(idx index, value *yaml.Node, root *yaml.Node) []*yaml.Node { + result := []*yaml.Node{} + + switch s.kind { + case segmentDotWildcard: + // Handle wildcard - get all children + switch value.Kind { + case yaml.MappingNode: + // in a mapping node, keys and values alternate + // we just want to return the values + for i, child := range value.Content { + if i%2 == 1 { + idx.setPropertyKey(value.Content[i-1], value) + idx.setPropertyKey(child, value.Content[i-1]) + result = append(result, child) + } + } + case yaml.SequenceNode: + for _, child := range value.Content { + result = append(result, child) + } + } + return result + case segmentDotMemberName: + // Handle member access + if value.Kind == yaml.MappingNode { + // In YAML mapping nodes, keys and values alternate + + for i := 0; i < len(value.Content); i += 2 { + key := value.Content[i] + val := value.Content[i+1] + + if key.Value == s.dotName { + idx.setPropertyKey(key, value) + idx.setPropertyKey(val, key) + result = append(result, val) + break + } + } + } + + case segmentLongHand: + // Handle long hand selectors + for _, selector := range s.selectors { + result = append(result, selector.Query(idx, value, root)...) + } + default: + panic("unknown child segment kind") + } + + return result + +} + +func (s selector) Query(idx index, value *yaml.Node, root *yaml.Node) []*yaml.Node { + switch s.kind { + case selectorSubKindName: + if value.Kind != yaml.MappingNode { + return nil + } + // MappingNode children is a list of alternating keys and values + var key string + for i, child := range value.Content { + if i%2 == 0 { + key = child.Value + continue + } + if key == s.name && i%2 == 1 { + idx.setPropertyKey(value.Content[i], value.Content[i-1]) + idx.setPropertyKey(value.Content[i-1], value) + return []*yaml.Node{child} + } + } + case selectorSubKindArrayIndex: + if value.Kind != yaml.SequenceNode { + return nil + } + // if out of bounds, return nothing + if s.index >= int64(len(value.Content)) || s.index < -int64(len(value.Content)) { + return nil + } + // if index is negative, go backwards + if s.index < 0 { + return []*yaml.Node{value.Content[int64(len(value.Content))+s.index]} + } + return []*yaml.Node{value.Content[s.index]} + case selectorSubKindWildcard: + if value.Kind == yaml.SequenceNode { + return value.Content + } else if value.Kind == yaml.MappingNode { + var result []*yaml.Node + for i, child := range value.Content { + if i%2 == 1 { + idx.setPropertyKey(value.Content[i-1], value) + idx.setPropertyKey(child, value.Content[i-1]) + result = append(result, child) + } + } + return result + } + return nil + case selectorSubKindArraySlice: + if value.Kind != yaml.SequenceNode { + return nil + } + if len(value.Content) == 0 { + return nil + } + step := int64(1) + if s.slice.step != nil { + step = *s.slice.step + } + if step == 0 { + return nil + } + + start, end := s.slice.start, s.slice.end + lower, upper := bounds(start, end, step, int64(len(value.Content))) + + var result []*yaml.Node + if step > 0 { + for i := lower; i < upper; i += step { + result = append(result, value.Content[i]) + } + } else { + for i := upper; i > lower; i += step { + result = append(result, value.Content[i]) + } + } + + return result + case selectorSubKindFilter: + var result []*yaml.Node + switch value.Kind { + case yaml.MappingNode: + for i := 1; i < len(value.Content); i += 2 { + idx.setPropertyKey(value.Content[i-1], value) + idx.setPropertyKey(value.Content[i], value.Content[i-1]) + if s.filter.Matches(idx, value.Content[i], root) { + result = append(result, value.Content[i]) + } + } + case yaml.SequenceNode: + for _, child := range value.Content { + if s.filter.Matches(idx, child, root) { + result = append(result, child) + } + } + } + return result + } + return nil +} + +func normalize(i, length int64) int64 { + if i >= 0 { + return i + } + return length + i +} + +func bounds(start, end *int64, step, length int64) (int64, int64) { + var nStart, nEnd int64 + if start != nil { + nStart = normalize(*start, length) + } else if step > 0 { + nStart = 0 + } else { + nStart = length - 1 + } + if end != nil { + nEnd = normalize(*end, length) + } else if step > 0 { + nEnd = length + } else { + nEnd = -1 + } + + var lower, upper int64 + if step >= 0 { + lower = max(min(nStart, length), 0) + upper = min(max(nEnd, 0), length) + } else { + upper = min(max(nStart, -1), length-1) + lower = min(max(nEnd, -1), length-1) + } + + return lower, upper +} + +func (s filterSelector) Matches(idx index, node *yaml.Node, root *yaml.Node) bool { + return s.expression.Matches(idx, node, root) +} + +func (e logicalOrExpr) Matches(idx index, node *yaml.Node, root *yaml.Node) bool { + for _, expr := range e.expressions { + if expr.Matches(idx, node, root) { + return true + } + } + return false +} + +func (e logicalAndExpr) Matches(idx index, node *yaml.Node, root *yaml.Node) bool { + for _, expr := range e.expressions { + if !expr.Matches(idx, node, root) { + return false + } + } + return true +} + +func (e basicExpr) Matches(idx index, node *yaml.Node, root *yaml.Node) bool { + if e.parenExpr != nil { + result := e.parenExpr.expr.Matches(idx, node, root) + if e.parenExpr.not { + return !result + } + return result + } else if e.comparisonExpr != nil { + return e.comparisonExpr.Matches(idx, node, root) + } else if e.testExpr != nil { + return e.testExpr.Matches(idx, node, root) + } + return false +} + +func (e comparisonExpr) Matches(idx index, node *yaml.Node, root *yaml.Node) bool { + leftValue := e.left.Evaluate(idx, node, root) + rightValue := e.right.Evaluate(idx, node, root) + + switch e.op { + case equalTo: + return leftValue.Equals(rightValue) + case notEqualTo: + return !leftValue.Equals(rightValue) + case lessThan: + return leftValue.LessThan(rightValue) + case lessThanEqualTo: + return leftValue.LessThanOrEqual(rightValue) + case greaterThan: + return rightValue.LessThan(leftValue) + case greaterThanEqualTo: + return rightValue.LessThanOrEqual(leftValue) + default: + return false + } +} + +func (e testExpr) Matches(idx index, node *yaml.Node, root *yaml.Node) bool { + var result bool + if e.filterQuery != nil { + result = len(e.filterQuery.Query(idx, node, root)) > 0 + } else if e.functionExpr != nil { + funcResult := e.functionExpr.Evaluate(idx, node, root) + if funcResult.bool != nil { + result = *funcResult.bool + } else if funcResult.null == nil { + result = true + } + } + if e.not { + return !result + } + return result +} + +func (q filterQuery) Query(idx index, node *yaml.Node, root *yaml.Node) []*yaml.Node { + if q.relQuery != nil { + return q.relQuery.Query(idx, node, root) + } + if q.jsonPathQuery != nil { + return q.jsonPathQuery.Query(node, root) + } + return nil +} + +func (q relQuery) Query(idx index, node *yaml.Node, root *yaml.Node) []*yaml.Node { + result := []*yaml.Node{node} + for _, seg := range q.segments { + var newResult []*yaml.Node + for _, value := range result { + newResult = append(newResult, seg.Query(idx, value, root)...) + } + result = newResult + } + return result +} + +func (q absQuery) Query(idx index, node *yaml.Node, root *yaml.Node) []*yaml.Node { + result := []*yaml.Node{root} + for _, seg := range q.segments { + var newResult []*yaml.Node + for _, value := range result { + newResult = append(newResult, seg.Query(idx, value, root)...) + } + result = newResult + } + return result +} diff --git a/vendor/github.com/speakeasy-api/openapi-overlay/pkg/overlay/apply.go b/vendor/github.com/speakeasy-api/openapi-overlay/pkg/overlay/apply.go index 8cd9aae..faaaf92 100644 --- a/vendor/github.com/speakeasy-api/openapi-overlay/pkg/overlay/apply.go +++ b/vendor/github.com/speakeasy-api/openapi-overlay/pkg/overlay/apply.go @@ -2,7 +2,8 @@ package overlay import ( "fmt" - "github.com/vmware-labs/yaml-jsonpath/pkg/yamlpath" + "github.com/speakeasy-api/jsonpath/pkg/jsonpath/config" + "github.com/speakeasy-api/jsonpath/pkg/jsonpath/token" "gopkg.in/yaml.v3" "strings" ) @@ -13,9 +14,9 @@ func (o *Overlay) ApplyTo(root *yaml.Node) error { for _, action := range o.Actions { var err error if action.Remove { - err = applyRemoveAction(root, action) + err = o.applyRemoveAction(root, action, nil) } else { - err = applyUpdateAction(root, action, &[]string{}) + err = o.applyUpdateAction(root, action, &[]string{}) } if err != nil { @@ -29,41 +30,51 @@ func (o *Overlay) ApplyTo(root *yaml.Node) error { func (o *Overlay) ApplyToStrict(root *yaml.Node) (error, []string) { multiError := []string{} warnings := []string{} + hasFilterExpression := false for i, action := range o.Actions { - err := validateSelectorHasAtLeastOneTarget(root, action) + tokens := token.NewTokenizer(action.Target, config.WithPropertyNameExtension()).Tokenize() + for _, tok := range tokens { + if tok.Token == token.FILTER { + hasFilterExpression = true + } + } + + actionWarnings := []string{} + err := o.validateSelectorHasAtLeastOneTarget(root, action) if err != nil { multiError = append(multiError, err.Error()) } if action.Remove { - err = applyRemoveAction(root, action) + err = o.applyRemoveAction(root, action, &actionWarnings) } else { - actionWarnings := []string{} - err = applyUpdateAction(root, action, &actionWarnings) - for _, warning := range actionWarnings { - warnings = append(warnings, fmt.Sprintf("update action (%v / %v) target=%s: %s", i+1, len(o.Actions), action.Target, warning)) - } + err = o.applyUpdateAction(root, action, &actionWarnings) + } + for _, warning := range actionWarnings { + warnings = append(warnings, fmt.Sprintf("update action (%v / %v) target=%s: %s", i+1, len(o.Actions), action.Target, warning)) } } + + if hasFilterExpression && !o.UsesRFC9535() { + warnings = append(warnings, "overlay has a filter expression but lacks `x-speakeasy-jsonpath: rfc9535` extension. Deprecated jsonpath behaviour in use. See overlay.speakeasy.com for the implementation playground.") + } + if len(multiError) > 0 { return fmt.Errorf("error applying overlay (strict): %v", strings.Join(multiError, ",")), warnings } return nil, warnings } -func validateSelectorHasAtLeastOneTarget(root *yaml.Node, action Action) error { +func (o *Overlay) validateSelectorHasAtLeastOneTarget(root *yaml.Node, action Action) error { if action.Target == "" { return nil } - p, err := yamlpath.NewPath(action.Target) + p, err := o.NewPath(action.Target, nil) if err != nil { return err } - nodes, err := p.Find(root) - if err != nil { - return err - } + nodes := p.Query(root) if len(nodes) == 0 { return fmt.Errorf("selector %q did not match any targets", action.Target) @@ -72,19 +83,19 @@ func validateSelectorHasAtLeastOneTarget(root *yaml.Node, action Action) error { return nil } -func applyRemoveAction(root *yaml.Node, action Action) error { +func (o *Overlay) applyRemoveAction(root *yaml.Node, action Action, warnings *[]string) error { if action.Target == "" { return nil } idx := newParentIndex(root) - p, err := yamlpath.NewPath(action.Target) + p, err := o.NewPath(action.Target, warnings) if err != nil { return err } - nodes, err := p.Find(root) + nodes := p.Query(root) if err != nil { return err } @@ -106,8 +117,13 @@ func removeNode(idx parentIndex, node *yaml.Node) { if child == node { switch parent.Kind { case yaml.MappingNode: - // we have to delete the key too - parent.Content = append(parent.Content[:i-1], parent.Content[i+1:]...) + if i%2 == 1 { + // if we select a value, we should delete the key too + parent.Content = append(parent.Content[:i-1], parent.Content[i+1:]...) + } else { + // if we select a key, we should delete the value + parent.Content = append(parent.Content[:i], parent.Content[i+2:]...) + } return case yaml.SequenceNode: parent.Content = append(parent.Content[:i], parent.Content[i+1:]...) @@ -117,7 +133,7 @@ func removeNode(idx parentIndex, node *yaml.Node) { } } -func applyUpdateAction(root *yaml.Node, action Action, warnings *[]string) error { +func (o *Overlay) applyUpdateAction(root *yaml.Node, action Action, warnings *[]string) error { if action.Target == "" { return nil } @@ -126,22 +142,19 @@ func applyUpdateAction(root *yaml.Node, action Action, warnings *[]string) error return nil } - p, err := yamlpath.NewPath(action.Target) - if err != nil { - return err - } - - nodes, err := p.Find(root) + p, err := o.NewPath(action.Target, warnings) if err != nil { return err } + nodes := p.Query(root) prior, err := yaml.Marshal(root) if err != nil { return err } + for _, node := range nodes { - if err := updateNode(node, action.Update); err != nil { + if err := updateNode(node, &action.Update); err != nil { return err } } @@ -156,14 +169,14 @@ func applyUpdateAction(root *yaml.Node, action Action, warnings *[]string) error return nil } -func updateNode(node *yaml.Node, updateNode yaml.Node) error { +func updateNode(node *yaml.Node, updateNode *yaml.Node) error { mergeNode(node, updateNode) return nil } -func mergeNode(node *yaml.Node, merge yaml.Node) { +func mergeNode(node *yaml.Node, merge *yaml.Node) { if node.Kind != merge.Kind { - *node = merge + *node = *clone(merge) return } switch node.Kind { @@ -178,7 +191,7 @@ func mergeNode(node *yaml.Node, merge yaml.Node) { // mergeMappingNode will perform a shallow merge of the merge node into the main // node. -func mergeMappingNode(node *yaml.Node, merge yaml.Node) { +func mergeMappingNode(node *yaml.Node, merge *yaml.Node) { NextKey: for i := 0; i < len(merge.Content); i += 2 { mergeKey := merge.Content[i].Value @@ -187,16 +200,39 @@ NextKey: for j := 0; j < len(node.Content); j += 2 { nodeKey := node.Content[j].Value if nodeKey == mergeKey { - mergeNode(node.Content[j+1], *mergeValue) + mergeNode(node.Content[j+1], mergeValue) continue NextKey } } - node.Content = append(node.Content, merge.Content[i], mergeValue) + node.Content = append(node.Content, merge.Content[i], clone(mergeValue)) } } // mergeSequenceNode will append the merge node's content to the original node. -func mergeSequenceNode(node *yaml.Node, merge yaml.Node) { - node.Content = append(node.Content, merge.Content...) +func mergeSequenceNode(node *yaml.Node, merge *yaml.Node) { + node.Content = append(node.Content, clone(merge).Content...) +} + +func clone(node *yaml.Node) *yaml.Node { + newNode := &yaml.Node{ + Kind: node.Kind, + Style: node.Style, + Tag: node.Tag, + Value: node.Value, + Anchor: node.Anchor, + HeadComment: node.HeadComment, + LineComment: node.LineComment, + FootComment: node.FootComment, + } + if node.Alias != nil { + newNode.Alias = clone(node.Alias) + } + if node.Content != nil { + newNode.Content = make([]*yaml.Node, len(node.Content)) + for i, child := range node.Content { + newNode.Content[i] = clone(child) + } + } + return newNode } diff --git a/vendor/github.com/speakeasy-api/openapi-overlay/pkg/overlay/compare.go b/vendor/github.com/speakeasy-api/openapi-overlay/pkg/overlay/compare.go index 1aa8d2f..33dff6a 100644 --- a/vendor/github.com/speakeasy-api/openapi-overlay/pkg/overlay/compare.go +++ b/vendor/github.com/speakeasy-api/openapi-overlay/pkg/overlay/compare.go @@ -18,7 +18,8 @@ func Compare(title string, y1 *yaml.Node, y2 yaml.Node) (*Overlay, error) { } return &Overlay{ - Version: "1.0.0", + Version: "1.0.0", + JSONPathVersion: "rfc9535", Info: Info{ Title: title, Version: "0.0.0", diff --git a/vendor/github.com/speakeasy-api/openapi-overlay/pkg/overlay/jsonpath.go b/vendor/github.com/speakeasy-api/openapi-overlay/pkg/overlay/jsonpath.go new file mode 100644 index 0000000..d43617c --- /dev/null +++ b/vendor/github.com/speakeasy-api/openapi-overlay/pkg/overlay/jsonpath.go @@ -0,0 +1,47 @@ +package overlay + +import ( + "fmt" + "github.com/speakeasy-api/jsonpath/pkg/jsonpath" + "github.com/speakeasy-api/jsonpath/pkg/jsonpath/config" + "github.com/vmware-labs/yaml-jsonpath/pkg/yamlpath" + "gopkg.in/yaml.v3" +) + +type Queryable interface { + Query(root *yaml.Node) []*yaml.Node +} + +type yamlPathQueryable struct { + path *yamlpath.Path +} + +func (y yamlPathQueryable) Query(root *yaml.Node) []*yaml.Node { + if y.path == nil { + return []*yaml.Node{} + } + // errors aren't actually possible from yamlpath. + result, _ := y.path.Find(root) + return result +} + +func (o *Overlay) NewPath(target string, warnings *[]string) (Queryable, error) { + rfcJSONPath, rfcJSONPathErr := jsonpath.NewPath(target, config.WithPropertyNameExtension()) + if o.UsesRFC9535() { + return rfcJSONPath, rfcJSONPathErr + } + if rfcJSONPathErr != nil && warnings != nil { + *warnings = append(*warnings, fmt.Sprintf("invalid rfc9535 jsonpath %s: %s\nThis will be treated as an error in the future. Please fix and opt into the new implementation with `\"x-speakeasy-jsonpath\": rfc9535` in the root of your overlay. See overlay.speakeasy.com for an implementation playground.", target, rfcJSONPathErr.Error())) + } + + path, err := yamlpath.NewPath(target) + return mustExecute(path), err +} + +func (o *Overlay) UsesRFC9535() bool { + return o.JSONPathVersion == "rfc9535" +} + +func mustExecute(path *yamlpath.Path) yamlPathQueryable { + return yamlPathQueryable{path} +} diff --git a/vendor/github.com/speakeasy-api/openapi-overlay/pkg/overlay/schema.go b/vendor/github.com/speakeasy-api/openapi-overlay/pkg/overlay/schema.go index db38422..f4408f0 100644 --- a/vendor/github.com/speakeasy-api/openapi-overlay/pkg/overlay/schema.go +++ b/vendor/github.com/speakeasy-api/openapi-overlay/pkg/overlay/schema.go @@ -13,10 +13,12 @@ type Extensions map[string]any type Overlay struct { Extensions `yaml:"-,inline"` - // Version is the version of the overlay configuration. As the RFC was never - // really ratifies, this value does not mean much. + // Version is the version of the overlay configuration. This is only ever expected to be 1.0.0 Version string `yaml:"overlay"` + // JSONPathVersion should be set to rfc9535, and is used for backwards compatability purposes + JSONPathVersion string `yaml:"x-speakeasy-jsonpath,omitempty"` + // Info describes the metadata for the overlay. Info Info `yaml:"info"` diff --git a/vendor/golang.org/x/mod/modfile/print.go b/vendor/golang.org/x/mod/modfile/print.go new file mode 100644 index 0000000..2a0123d --- /dev/null +++ b/vendor/golang.org/x/mod/modfile/print.go @@ -0,0 +1,184 @@ +// Copyright 2018 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Module file printer. + +package modfile + +import ( + "bytes" + "fmt" + "strings" +) + +// Format returns a go.mod file as a byte slice, formatted in standard style. +func Format(f *FileSyntax) []byte { + pr := &printer{} + pr.file(f) + + // remove trailing blank lines + b := pr.Bytes() + for len(b) > 0 && b[len(b)-1] == '\n' && (len(b) == 1 || b[len(b)-2] == '\n') { + b = b[:len(b)-1] + } + return b +} + +// A printer collects the state during printing of a file or expression. +type printer struct { + bytes.Buffer // output buffer + comment []Comment // pending end-of-line comments + margin int // left margin (indent), a number of tabs +} + +// printf prints to the buffer. +func (p *printer) printf(format string, args ...interface{}) { + fmt.Fprintf(p, format, args...) +} + +// indent returns the position on the current line, in bytes, 0-indexed. +func (p *printer) indent() int { + b := p.Bytes() + n := 0 + for n < len(b) && b[len(b)-1-n] != '\n' { + n++ + } + return n +} + +// newline ends the current line, flushing end-of-line comments. +func (p *printer) newline() { + if len(p.comment) > 0 { + p.printf(" ") + for i, com := range p.comment { + if i > 0 { + p.trim() + p.printf("\n") + for i := 0; i < p.margin; i++ { + p.printf("\t") + } + } + p.printf("%s", strings.TrimSpace(com.Token)) + } + p.comment = p.comment[:0] + } + + p.trim() + if b := p.Bytes(); len(b) == 0 || (len(b) >= 2 && b[len(b)-1] == '\n' && b[len(b)-2] == '\n') { + // skip the blank line at top of file or after a blank line + } else { + p.printf("\n") + } + for i := 0; i < p.margin; i++ { + p.printf("\t") + } +} + +// trim removes trailing spaces and tabs from the current line. +func (p *printer) trim() { + // Remove trailing spaces and tabs from line we're about to end. + b := p.Bytes() + n := len(b) + for n > 0 && (b[n-1] == '\t' || b[n-1] == ' ') { + n-- + } + p.Truncate(n) +} + +// file formats the given file into the print buffer. +func (p *printer) file(f *FileSyntax) { + for _, com := range f.Before { + p.printf("%s", strings.TrimSpace(com.Token)) + p.newline() + } + + for i, stmt := range f.Stmt { + switch x := stmt.(type) { + case *CommentBlock: + // comments already handled + p.expr(x) + + default: + p.expr(x) + p.newline() + } + + for _, com := range stmt.Comment().After { + p.printf("%s", strings.TrimSpace(com.Token)) + p.newline() + } + + if i+1 < len(f.Stmt) { + p.newline() + } + } +} + +func (p *printer) expr(x Expr) { + // Emit line-comments preceding this expression. + if before := x.Comment().Before; len(before) > 0 { + // Want to print a line comment. + // Line comments must be at the current margin. + p.trim() + if p.indent() > 0 { + // There's other text on the line. Start a new line. + p.printf("\n") + } + // Re-indent to margin. + for i := 0; i < p.margin; i++ { + p.printf("\t") + } + for _, com := range before { + p.printf("%s", strings.TrimSpace(com.Token)) + p.newline() + } + } + + switch x := x.(type) { + default: + panic(fmt.Errorf("printer: unexpected type %T", x)) + + case *CommentBlock: + // done + + case *LParen: + p.printf("(") + case *RParen: + p.printf(")") + + case *Line: + p.tokens(x.Token) + + case *LineBlock: + p.tokens(x.Token) + p.printf(" ") + p.expr(&x.LParen) + p.margin++ + for _, l := range x.Line { + p.newline() + p.expr(l) + } + p.margin-- + p.newline() + p.expr(&x.RParen) + } + + // Queue end-of-line comments for printing when we + // reach the end of the line. + p.comment = append(p.comment, x.Comment().Suffix...) +} + +func (p *printer) tokens(tokens []string) { + sep := "" + for _, t := range tokens { + if t == "," || t == ")" || t == "]" || t == "}" { + sep = "" + } + p.printf("%s%s", sep, t) + sep = " " + if t == "(" || t == "[" || t == "{" { + sep = "" + } + } +} diff --git a/vendor/golang.org/x/mod/modfile/read.go b/vendor/golang.org/x/mod/modfile/read.go new file mode 100644 index 0000000..2d74868 --- /dev/null +++ b/vendor/golang.org/x/mod/modfile/read.go @@ -0,0 +1,964 @@ +// Copyright 2018 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package modfile + +import ( + "bytes" + "errors" + "fmt" + "os" + "strconv" + "strings" + "unicode" + "unicode/utf8" +) + +// A Position describes an arbitrary source position in a file, including the +// file, line, column, and byte offset. +type Position struct { + Line int // line in input (starting at 1) + LineRune int // rune in line (starting at 1) + Byte int // byte in input (starting at 0) +} + +// add returns the position at the end of s, assuming it starts at p. +func (p Position) add(s string) Position { + p.Byte += len(s) + if n := strings.Count(s, "\n"); n > 0 { + p.Line += n + s = s[strings.LastIndex(s, "\n")+1:] + p.LineRune = 1 + } + p.LineRune += utf8.RuneCountInString(s) + return p +} + +// An Expr represents an input element. +type Expr interface { + // Span returns the start and end position of the expression, + // excluding leading or trailing comments. + Span() (start, end Position) + + // Comment returns the comments attached to the expression. + // This method would normally be named 'Comments' but that + // would interfere with embedding a type of the same name. + Comment() *Comments +} + +// A Comment represents a single // comment. +type Comment struct { + Start Position + Token string // without trailing newline + Suffix bool // an end of line (not whole line) comment +} + +// Comments collects the comments associated with an expression. +type Comments struct { + Before []Comment // whole-line comments before this expression + Suffix []Comment // end-of-line comments after this expression + + // For top-level expressions only, After lists whole-line + // comments following the expression. + After []Comment +} + +// Comment returns the receiver. This isn't useful by itself, but +// a [Comments] struct is embedded into all the expression +// implementation types, and this gives each of those a Comment +// method to satisfy the Expr interface. +func (c *Comments) Comment() *Comments { + return c +} + +// A FileSyntax represents an entire go.mod file. +type FileSyntax struct { + Name string // file path + Comments + Stmt []Expr +} + +func (x *FileSyntax) Span() (start, end Position) { + if len(x.Stmt) == 0 { + return + } + start, _ = x.Stmt[0].Span() + _, end = x.Stmt[len(x.Stmt)-1].Span() + return start, end +} + +// addLine adds a line containing the given tokens to the file. +// +// If the first token of the hint matches the first token of the +// line, the new line is added at the end of the block containing hint, +// extracting hint into a new block if it is not yet in one. +// +// If the hint is non-nil buts its first token does not match, +// the new line is added after the block containing hint +// (or hint itself, if not in a block). +// +// If no hint is provided, addLine appends the line to the end of +// the last block with a matching first token, +// or to the end of the file if no such block exists. +func (x *FileSyntax) addLine(hint Expr, tokens ...string) *Line { + if hint == nil { + // If no hint given, add to the last statement of the given type. + Loop: + for i := len(x.Stmt) - 1; i >= 0; i-- { + stmt := x.Stmt[i] + switch stmt := stmt.(type) { + case *Line: + if stmt.Token != nil && stmt.Token[0] == tokens[0] { + hint = stmt + break Loop + } + case *LineBlock: + if stmt.Token[0] == tokens[0] { + hint = stmt + break Loop + } + } + } + } + + newLineAfter := func(i int) *Line { + new := &Line{Token: tokens} + if i == len(x.Stmt) { + x.Stmt = append(x.Stmt, new) + } else { + x.Stmt = append(x.Stmt, nil) + copy(x.Stmt[i+2:], x.Stmt[i+1:]) + x.Stmt[i+1] = new + } + return new + } + + if hint != nil { + for i, stmt := range x.Stmt { + switch stmt := stmt.(type) { + case *Line: + if stmt == hint { + if stmt.Token == nil || stmt.Token[0] != tokens[0] { + return newLineAfter(i) + } + + // Convert line to line block. + stmt.InBlock = true + block := &LineBlock{Token: stmt.Token[:1], Line: []*Line{stmt}} + stmt.Token = stmt.Token[1:] + x.Stmt[i] = block + new := &Line{Token: tokens[1:], InBlock: true} + block.Line = append(block.Line, new) + return new + } + + case *LineBlock: + if stmt == hint { + if stmt.Token[0] != tokens[0] { + return newLineAfter(i) + } + + new := &Line{Token: tokens[1:], InBlock: true} + stmt.Line = append(stmt.Line, new) + return new + } + + for j, line := range stmt.Line { + if line == hint { + if stmt.Token[0] != tokens[0] { + return newLineAfter(i) + } + + // Add new line after hint within the block. + stmt.Line = append(stmt.Line, nil) + copy(stmt.Line[j+2:], stmt.Line[j+1:]) + new := &Line{Token: tokens[1:], InBlock: true} + stmt.Line[j+1] = new + return new + } + } + } + } + } + + new := &Line{Token: tokens} + x.Stmt = append(x.Stmt, new) + return new +} + +func (x *FileSyntax) updateLine(line *Line, tokens ...string) { + if line.InBlock { + tokens = tokens[1:] + } + line.Token = tokens +} + +// markRemoved modifies line so that it (and its end-of-line comment, if any) +// will be dropped by (*FileSyntax).Cleanup. +func (line *Line) markRemoved() { + line.Token = nil + line.Comments.Suffix = nil +} + +// Cleanup cleans up the file syntax x after any edit operations. +// To avoid quadratic behavior, (*Line).markRemoved marks the line as dead +// by setting line.Token = nil but does not remove it from the slice +// in which it appears. After edits have all been indicated, +// calling Cleanup cleans out the dead lines. +func (x *FileSyntax) Cleanup() { + w := 0 + for _, stmt := range x.Stmt { + switch stmt := stmt.(type) { + case *Line: + if stmt.Token == nil { + continue + } + case *LineBlock: + ww := 0 + for _, line := range stmt.Line { + if line.Token != nil { + stmt.Line[ww] = line + ww++ + } + } + if ww == 0 { + continue + } + if ww == 1 && len(stmt.RParen.Comments.Before) == 0 { + // Collapse block into single line but keep the Line reference used by the + // parsed File structure. + *stmt.Line[0] = Line{ + Comments: Comments{ + Before: commentsAdd(stmt.Before, stmt.Line[0].Before), + Suffix: commentsAdd(stmt.Line[0].Suffix, stmt.Suffix), + After: commentsAdd(stmt.Line[0].After, stmt.After), + }, + Token: stringsAdd(stmt.Token, stmt.Line[0].Token), + } + x.Stmt[w] = stmt.Line[0] + w++ + continue + } + stmt.Line = stmt.Line[:ww] + } + x.Stmt[w] = stmt + w++ + } + x.Stmt = x.Stmt[:w] +} + +func commentsAdd(x, y []Comment) []Comment { + return append(x[:len(x):len(x)], y...) +} + +func stringsAdd(x, y []string) []string { + return append(x[:len(x):len(x)], y...) +} + +// A CommentBlock represents a top-level block of comments separate +// from any rule. +type CommentBlock struct { + Comments + Start Position +} + +func (x *CommentBlock) Span() (start, end Position) { + return x.Start, x.Start +} + +// A Line is a single line of tokens. +type Line struct { + Comments + Start Position + Token []string + InBlock bool + End Position +} + +func (x *Line) Span() (start, end Position) { + return x.Start, x.End +} + +// A LineBlock is a factored block of lines, like +// +// require ( +// "x" +// "y" +// ) +type LineBlock struct { + Comments + Start Position + LParen LParen + Token []string + Line []*Line + RParen RParen +} + +func (x *LineBlock) Span() (start, end Position) { + return x.Start, x.RParen.Pos.add(")") +} + +// An LParen represents the beginning of a parenthesized line block. +// It is a place to store suffix comments. +type LParen struct { + Comments + Pos Position +} + +func (x *LParen) Span() (start, end Position) { + return x.Pos, x.Pos.add(")") +} + +// An RParen represents the end of a parenthesized line block. +// It is a place to store whole-line (before) comments. +type RParen struct { + Comments + Pos Position +} + +func (x *RParen) Span() (start, end Position) { + return x.Pos, x.Pos.add(")") +} + +// An input represents a single input file being parsed. +type input struct { + // Lexing state. + filename string // name of input file, for errors + complete []byte // entire input + remaining []byte // remaining input + tokenStart []byte // token being scanned to end of input + token token // next token to be returned by lex, peek + pos Position // current input position + comments []Comment // accumulated comments + + // Parser state. + file *FileSyntax // returned top-level syntax tree + parseErrors ErrorList // errors encountered during parsing + + // Comment assignment state. + pre []Expr // all expressions, in preorder traversal + post []Expr // all expressions, in postorder traversal +} + +func newInput(filename string, data []byte) *input { + return &input{ + filename: filename, + complete: data, + remaining: data, + pos: Position{Line: 1, LineRune: 1, Byte: 0}, + } +} + +// parse parses the input file. +func parse(file string, data []byte) (f *FileSyntax, err error) { + // The parser panics for both routine errors like syntax errors + // and for programmer bugs like array index errors. + // Turn both into error returns. Catching bug panics is + // especially important when processing many files. + in := newInput(file, data) + defer func() { + if e := recover(); e != nil && e != &in.parseErrors { + in.parseErrors = append(in.parseErrors, Error{ + Filename: in.filename, + Pos: in.pos, + Err: fmt.Errorf("internal error: %v", e), + }) + } + if err == nil && len(in.parseErrors) > 0 { + err = in.parseErrors + } + }() + + // Prime the lexer by reading in the first token. It will be available + // in the next peek() or lex() call. + in.readToken() + + // Invoke the parser. + in.parseFile() + if len(in.parseErrors) > 0 { + return nil, in.parseErrors + } + in.file.Name = in.filename + + // Assign comments to nearby syntax. + in.assignComments() + + return in.file, nil +} + +// Error is called to report an error. +// Error does not return: it panics. +func (in *input) Error(s string) { + in.parseErrors = append(in.parseErrors, Error{ + Filename: in.filename, + Pos: in.pos, + Err: errors.New(s), + }) + panic(&in.parseErrors) +} + +// eof reports whether the input has reached end of file. +func (in *input) eof() bool { + return len(in.remaining) == 0 +} + +// peekRune returns the next rune in the input without consuming it. +func (in *input) peekRune() int { + if len(in.remaining) == 0 { + return 0 + } + r, _ := utf8.DecodeRune(in.remaining) + return int(r) +} + +// peekPrefix reports whether the remaining input begins with the given prefix. +func (in *input) peekPrefix(prefix string) bool { + // This is like bytes.HasPrefix(in.remaining, []byte(prefix)) + // but without the allocation of the []byte copy of prefix. + for i := 0; i < len(prefix); i++ { + if i >= len(in.remaining) || in.remaining[i] != prefix[i] { + return false + } + } + return true +} + +// readRune consumes and returns the next rune in the input. +func (in *input) readRune() int { + if len(in.remaining) == 0 { + in.Error("internal lexer error: readRune at EOF") + } + r, size := utf8.DecodeRune(in.remaining) + in.remaining = in.remaining[size:] + if r == '\n' { + in.pos.Line++ + in.pos.LineRune = 1 + } else { + in.pos.LineRune++ + } + in.pos.Byte += size + return int(r) +} + +type token struct { + kind tokenKind + pos Position + endPos Position + text string +} + +type tokenKind int + +const ( + _EOF tokenKind = -(iota + 1) + _EOLCOMMENT + _IDENT + _STRING + _COMMENT + + // newlines and punctuation tokens are allowed as ASCII codes. +) + +func (k tokenKind) isComment() bool { + return k == _COMMENT || k == _EOLCOMMENT +} + +// isEOL returns whether a token terminates a line. +func (k tokenKind) isEOL() bool { + return k == _EOF || k == _EOLCOMMENT || k == '\n' +} + +// startToken marks the beginning of the next input token. +// It must be followed by a call to endToken, once the token's text has +// been consumed using readRune. +func (in *input) startToken() { + in.tokenStart = in.remaining + in.token.text = "" + in.token.pos = in.pos +} + +// endToken marks the end of an input token. +// It records the actual token string in tok.text. +// A single trailing newline (LF or CRLF) will be removed from comment tokens. +func (in *input) endToken(kind tokenKind) { + in.token.kind = kind + text := string(in.tokenStart[:len(in.tokenStart)-len(in.remaining)]) + if kind.isComment() { + if strings.HasSuffix(text, "\r\n") { + text = text[:len(text)-2] + } else { + text = strings.TrimSuffix(text, "\n") + } + } + in.token.text = text + in.token.endPos = in.pos +} + +// peek returns the kind of the next token returned by lex. +func (in *input) peek() tokenKind { + return in.token.kind +} + +// lex is called from the parser to obtain the next input token. +func (in *input) lex() token { + tok := in.token + in.readToken() + return tok +} + +// readToken lexes the next token from the text and stores it in in.token. +func (in *input) readToken() { + // Skip past spaces, stopping at non-space or EOF. + for !in.eof() { + c := in.peekRune() + if c == ' ' || c == '\t' || c == '\r' { + in.readRune() + continue + } + + // Comment runs to end of line. + if in.peekPrefix("//") { + in.startToken() + + // Is this comment the only thing on its line? + // Find the last \n before this // and see if it's all + // spaces from there to here. + i := bytes.LastIndex(in.complete[:in.pos.Byte], []byte("\n")) + suffix := len(bytes.TrimSpace(in.complete[i+1:in.pos.Byte])) > 0 + in.readRune() + in.readRune() + + // Consume comment. + for len(in.remaining) > 0 && in.readRune() != '\n' { + } + + // If we are at top level (not in a statement), hand the comment to + // the parser as a _COMMENT token. The grammar is written + // to handle top-level comments itself. + if !suffix { + in.endToken(_COMMENT) + return + } + + // Otherwise, save comment for later attachment to syntax tree. + in.endToken(_EOLCOMMENT) + in.comments = append(in.comments, Comment{in.token.pos, in.token.text, suffix}) + return + } + + if in.peekPrefix("/*") { + in.Error("mod files must use // comments (not /* */ comments)") + } + + // Found non-space non-comment. + break + } + + // Found the beginning of the next token. + in.startToken() + + // End of file. + if in.eof() { + in.endToken(_EOF) + return + } + + // Punctuation tokens. + switch c := in.peekRune(); c { + case '\n', '(', ')', '[', ']', '{', '}', ',': + in.readRune() + in.endToken(tokenKind(c)) + return + + case '"', '`': // quoted string + quote := c + in.readRune() + for { + if in.eof() { + in.pos = in.token.pos + in.Error("unexpected EOF in string") + } + if in.peekRune() == '\n' { + in.Error("unexpected newline in string") + } + c := in.readRune() + if c == quote { + break + } + if c == '\\' && quote != '`' { + if in.eof() { + in.pos = in.token.pos + in.Error("unexpected EOF in string") + } + in.readRune() + } + } + in.endToken(_STRING) + return + } + + // Checked all punctuation. Must be identifier token. + if c := in.peekRune(); !isIdent(c) { + in.Error(fmt.Sprintf("unexpected input character %#q", c)) + } + + // Scan over identifier. + for isIdent(in.peekRune()) { + if in.peekPrefix("//") { + break + } + if in.peekPrefix("/*") { + in.Error("mod files must use // comments (not /* */ comments)") + } + in.readRune() + } + in.endToken(_IDENT) +} + +// isIdent reports whether c is an identifier rune. +// We treat most printable runes as identifier runes, except for a handful of +// ASCII punctuation characters. +func isIdent(c int) bool { + switch r := rune(c); r { + case ' ', '(', ')', '[', ']', '{', '}', ',': + return false + default: + return !unicode.IsSpace(r) && unicode.IsPrint(r) + } +} + +// Comment assignment. +// We build two lists of all subexpressions, preorder and postorder. +// The preorder list is ordered by start location, with outer expressions first. +// The postorder list is ordered by end location, with outer expressions last. +// We use the preorder list to assign each whole-line comment to the syntax +// immediately following it, and we use the postorder list to assign each +// end-of-line comment to the syntax immediately preceding it. + +// order walks the expression adding it and its subexpressions to the +// preorder and postorder lists. +func (in *input) order(x Expr) { + if x != nil { + in.pre = append(in.pre, x) + } + switch x := x.(type) { + default: + panic(fmt.Errorf("order: unexpected type %T", x)) + case nil: + // nothing + case *LParen, *RParen: + // nothing + case *CommentBlock: + // nothing + case *Line: + // nothing + case *FileSyntax: + for _, stmt := range x.Stmt { + in.order(stmt) + } + case *LineBlock: + in.order(&x.LParen) + for _, l := range x.Line { + in.order(l) + } + in.order(&x.RParen) + } + if x != nil { + in.post = append(in.post, x) + } +} + +// assignComments attaches comments to nearby syntax. +func (in *input) assignComments() { + const debug = false + + // Generate preorder and postorder lists. + in.order(in.file) + + // Split into whole-line comments and suffix comments. + var line, suffix []Comment + for _, com := range in.comments { + if com.Suffix { + suffix = append(suffix, com) + } else { + line = append(line, com) + } + } + + if debug { + for _, c := range line { + fmt.Fprintf(os.Stderr, "LINE %q :%d:%d #%d\n", c.Token, c.Start.Line, c.Start.LineRune, c.Start.Byte) + } + } + + // Assign line comments to syntax immediately following. + for _, x := range in.pre { + start, _ := x.Span() + if debug { + fmt.Fprintf(os.Stderr, "pre %T :%d:%d #%d\n", x, start.Line, start.LineRune, start.Byte) + } + xcom := x.Comment() + for len(line) > 0 && start.Byte >= line[0].Start.Byte { + if debug { + fmt.Fprintf(os.Stderr, "ASSIGN LINE %q #%d\n", line[0].Token, line[0].Start.Byte) + } + xcom.Before = append(xcom.Before, line[0]) + line = line[1:] + } + } + + // Remaining line comments go at end of file. + in.file.After = append(in.file.After, line...) + + if debug { + for _, c := range suffix { + fmt.Fprintf(os.Stderr, "SUFFIX %q :%d:%d #%d\n", c.Token, c.Start.Line, c.Start.LineRune, c.Start.Byte) + } + } + + // Assign suffix comments to syntax immediately before. + for i := len(in.post) - 1; i >= 0; i-- { + x := in.post[i] + + start, end := x.Span() + if debug { + fmt.Fprintf(os.Stderr, "post %T :%d:%d #%d :%d:%d #%d\n", x, start.Line, start.LineRune, start.Byte, end.Line, end.LineRune, end.Byte) + } + + // Do not assign suffix comments to end of line block or whole file. + // Instead assign them to the last element inside. + switch x.(type) { + case *FileSyntax: + continue + } + + // Do not assign suffix comments to something that starts + // on an earlier line, so that in + // + // x ( y + // z ) // comment + // + // we assign the comment to z and not to x ( ... ). + if start.Line != end.Line { + continue + } + xcom := x.Comment() + for len(suffix) > 0 && end.Byte <= suffix[len(suffix)-1].Start.Byte { + if debug { + fmt.Fprintf(os.Stderr, "ASSIGN SUFFIX %q #%d\n", suffix[len(suffix)-1].Token, suffix[len(suffix)-1].Start.Byte) + } + xcom.Suffix = append(xcom.Suffix, suffix[len(suffix)-1]) + suffix = suffix[:len(suffix)-1] + } + } + + // We assigned suffix comments in reverse. + // If multiple suffix comments were appended to the same + // expression node, they are now in reverse. Fix that. + for _, x := range in.post { + reverseComments(x.Comment().Suffix) + } + + // Remaining suffix comments go at beginning of file. + in.file.Before = append(in.file.Before, suffix...) +} + +// reverseComments reverses the []Comment list. +func reverseComments(list []Comment) { + for i, j := 0, len(list)-1; i < j; i, j = i+1, j-1 { + list[i], list[j] = list[j], list[i] + } +} + +func (in *input) parseFile() { + in.file = new(FileSyntax) + var cb *CommentBlock + for { + switch in.peek() { + case '\n': + in.lex() + if cb != nil { + in.file.Stmt = append(in.file.Stmt, cb) + cb = nil + } + case _COMMENT: + tok := in.lex() + if cb == nil { + cb = &CommentBlock{Start: tok.pos} + } + com := cb.Comment() + com.Before = append(com.Before, Comment{Start: tok.pos, Token: tok.text}) + case _EOF: + if cb != nil { + in.file.Stmt = append(in.file.Stmt, cb) + } + return + default: + in.parseStmt() + if cb != nil { + in.file.Stmt[len(in.file.Stmt)-1].Comment().Before = cb.Before + cb = nil + } + } + } +} + +func (in *input) parseStmt() { + tok := in.lex() + start := tok.pos + end := tok.endPos + tokens := []string{tok.text} + for { + tok := in.lex() + switch { + case tok.kind.isEOL(): + in.file.Stmt = append(in.file.Stmt, &Line{ + Start: start, + Token: tokens, + End: end, + }) + return + + case tok.kind == '(': + if next := in.peek(); next.isEOL() { + // Start of block: no more tokens on this line. + in.file.Stmt = append(in.file.Stmt, in.parseLineBlock(start, tokens, tok)) + return + } else if next == ')' { + rparen := in.lex() + if in.peek().isEOL() { + // Empty block. + in.lex() + in.file.Stmt = append(in.file.Stmt, &LineBlock{ + Start: start, + Token: tokens, + LParen: LParen{Pos: tok.pos}, + RParen: RParen{Pos: rparen.pos}, + }) + return + } + // '( )' in the middle of the line, not a block. + tokens = append(tokens, tok.text, rparen.text) + } else { + // '(' in the middle of the line, not a block. + tokens = append(tokens, tok.text) + } + + default: + tokens = append(tokens, tok.text) + end = tok.endPos + } + } +} + +func (in *input) parseLineBlock(start Position, token []string, lparen token) *LineBlock { + x := &LineBlock{ + Start: start, + Token: token, + LParen: LParen{Pos: lparen.pos}, + } + var comments []Comment + for { + switch in.peek() { + case _EOLCOMMENT: + // Suffix comment, will be attached later by assignComments. + in.lex() + case '\n': + // Blank line. Add an empty comment to preserve it. + in.lex() + if len(comments) == 0 && len(x.Line) > 0 || len(comments) > 0 && comments[len(comments)-1].Token != "" { + comments = append(comments, Comment{}) + } + case _COMMENT: + tok := in.lex() + comments = append(comments, Comment{Start: tok.pos, Token: tok.text}) + case _EOF: + in.Error(fmt.Sprintf("syntax error (unterminated block started at %s:%d:%d)", in.filename, x.Start.Line, x.Start.LineRune)) + case ')': + rparen := in.lex() + // Don't preserve blank lines (denoted by a single empty comment, added above) + // at the end of the block. + if len(comments) == 1 && comments[0] == (Comment{}) { + comments = nil + } + x.RParen.Before = comments + x.RParen.Pos = rparen.pos + if !in.peek().isEOL() { + in.Error("syntax error (expected newline after closing paren)") + } + in.lex() + return x + default: + l := in.parseLine() + x.Line = append(x.Line, l) + l.Comment().Before = comments + comments = nil + } + } +} + +func (in *input) parseLine() *Line { + tok := in.lex() + if tok.kind.isEOL() { + in.Error("internal parse error: parseLine at end of line") + } + start := tok.pos + end := tok.endPos + tokens := []string{tok.text} + for { + tok := in.lex() + if tok.kind.isEOL() { + return &Line{ + Start: start, + Token: tokens, + End: end, + InBlock: true, + } + } + tokens = append(tokens, tok.text) + end = tok.endPos + } +} + +var ( + slashSlash = []byte("//") + moduleStr = []byte("module") +) + +// ModulePath returns the module path from the gomod file text. +// If it cannot find a module path, it returns an empty string. +// It is tolerant of unrelated problems in the go.mod file. +func ModulePath(mod []byte) string { + for len(mod) > 0 { + line := mod + mod = nil + if i := bytes.IndexByte(line, '\n'); i >= 0 { + line, mod = line[:i], line[i+1:] + } + if i := bytes.Index(line, slashSlash); i >= 0 { + line = line[:i] + } + line = bytes.TrimSpace(line) + if !bytes.HasPrefix(line, moduleStr) { + continue + } + line = line[len(moduleStr):] + n := len(line) + line = bytes.TrimSpace(line) + if len(line) == n || len(line) == 0 { + continue + } + + if line[0] == '"' || line[0] == '`' { + p, err := strconv.Unquote(string(line)) + if err != nil { + return "" // malformed quoted string or multiline module path + } + return p + } + + return string(line) + } + return "" // missing module path +} diff --git a/vendor/golang.org/x/mod/modfile/rule.go b/vendor/golang.org/x/mod/modfile/rule.go new file mode 100644 index 0000000..3e4a1d0 --- /dev/null +++ b/vendor/golang.org/x/mod/modfile/rule.go @@ -0,0 +1,1836 @@ +// Copyright 2018 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package modfile implements a parser and formatter for go.mod files. +// +// The go.mod syntax is described in +// https://pkg.go.dev/cmd/go/#hdr-The_go_mod_file. +// +// The [Parse] and [ParseLax] functions both parse a go.mod file and return an +// abstract syntax tree. ParseLax ignores unknown statements and may be used to +// parse go.mod files that may have been developed with newer versions of Go. +// +// The [File] struct returned by Parse and ParseLax represent an abstract +// go.mod file. File has several methods like [File.AddNewRequire] and +// [File.DropReplace] that can be used to programmatically edit a file. +// +// The [Format] function formats a File back to a byte slice which can be +// written to a file. +package modfile + +import ( + "errors" + "fmt" + "path/filepath" + "sort" + "strconv" + "strings" + "unicode" + + "golang.org/x/mod/internal/lazyregexp" + "golang.org/x/mod/module" + "golang.org/x/mod/semver" +) + +// A File is the parsed, interpreted form of a go.mod file. +type File struct { + Module *Module + Go *Go + Toolchain *Toolchain + Godebug []*Godebug + Require []*Require + Exclude []*Exclude + Replace []*Replace + Retract []*Retract + Tool []*Tool + + Syntax *FileSyntax +} + +// A Module is the module statement. +type Module struct { + Mod module.Version + Deprecated string + Syntax *Line +} + +// A Go is the go statement. +type Go struct { + Version string // "1.23" + Syntax *Line +} + +// A Toolchain is the toolchain statement. +type Toolchain struct { + Name string // "go1.21rc1" + Syntax *Line +} + +// A Godebug is a single godebug key=value statement. +type Godebug struct { + Key string + Value string + Syntax *Line +} + +// An Exclude is a single exclude statement. +type Exclude struct { + Mod module.Version + Syntax *Line +} + +// A Replace is a single replace statement. +type Replace struct { + Old module.Version + New module.Version + Syntax *Line +} + +// A Retract is a single retract statement. +type Retract struct { + VersionInterval + Rationale string + Syntax *Line +} + +// A Tool is a single tool statement. +type Tool struct { + Path string + Syntax *Line +} + +// A VersionInterval represents a range of versions with upper and lower bounds. +// Intervals are closed: both bounds are included. When Low is equal to High, +// the interval may refer to a single version ('v1.2.3') or an interval +// ('[v1.2.3, v1.2.3]'); both have the same representation. +type VersionInterval struct { + Low, High string +} + +// A Require is a single require statement. +type Require struct { + Mod module.Version + Indirect bool // has "// indirect" comment + Syntax *Line +} + +func (r *Require) markRemoved() { + r.Syntax.markRemoved() + *r = Require{} +} + +func (r *Require) setVersion(v string) { + r.Mod.Version = v + + if line := r.Syntax; len(line.Token) > 0 { + if line.InBlock { + // If the line is preceded by an empty line, remove it; see + // https://golang.org/issue/33779. + if len(line.Comments.Before) == 1 && len(line.Comments.Before[0].Token) == 0 { + line.Comments.Before = line.Comments.Before[:0] + } + if len(line.Token) >= 2 { // example.com v1.2.3 + line.Token[1] = v + } + } else { + if len(line.Token) >= 3 { // require example.com v1.2.3 + line.Token[2] = v + } + } + } +} + +// setIndirect sets line to have (or not have) a "// indirect" comment. +func (r *Require) setIndirect(indirect bool) { + r.Indirect = indirect + line := r.Syntax + if isIndirect(line) == indirect { + return + } + if indirect { + // Adding comment. + if len(line.Suffix) == 0 { + // New comment. + line.Suffix = []Comment{{Token: "// indirect", Suffix: true}} + return + } + + com := &line.Suffix[0] + text := strings.TrimSpace(strings.TrimPrefix(com.Token, string(slashSlash))) + if text == "" { + // Empty comment. + com.Token = "// indirect" + return + } + + // Insert at beginning of existing comment. + com.Token = "// indirect; " + text + return + } + + // Removing comment. + f := strings.TrimSpace(strings.TrimPrefix(line.Suffix[0].Token, string(slashSlash))) + if f == "indirect" { + // Remove whole comment. + line.Suffix = nil + return + } + + // Remove comment prefix. + com := &line.Suffix[0] + i := strings.Index(com.Token, "indirect;") + com.Token = "//" + com.Token[i+len("indirect;"):] +} + +// isIndirect reports whether line has a "// indirect" comment, +// meaning it is in go.mod only for its effect on indirect dependencies, +// so that it can be dropped entirely once the effective version of the +// indirect dependency reaches the given minimum version. +func isIndirect(line *Line) bool { + if len(line.Suffix) == 0 { + return false + } + f := strings.Fields(strings.TrimPrefix(line.Suffix[0].Token, string(slashSlash))) + return (len(f) == 1 && f[0] == "indirect" || len(f) > 1 && f[0] == "indirect;") +} + +func (f *File) AddModuleStmt(path string) error { + if f.Syntax == nil { + f.Syntax = new(FileSyntax) + } + if f.Module == nil { + f.Module = &Module{ + Mod: module.Version{Path: path}, + Syntax: f.Syntax.addLine(nil, "module", AutoQuote(path)), + } + } else { + f.Module.Mod.Path = path + f.Syntax.updateLine(f.Module.Syntax, "module", AutoQuote(path)) + } + return nil +} + +func (f *File) AddComment(text string) { + if f.Syntax == nil { + f.Syntax = new(FileSyntax) + } + f.Syntax.Stmt = append(f.Syntax.Stmt, &CommentBlock{ + Comments: Comments{ + Before: []Comment{ + { + Token: text, + }, + }, + }, + }) +} + +type VersionFixer func(path, version string) (string, error) + +// errDontFix is returned by a VersionFixer to indicate the version should be +// left alone, even if it's not canonical. +var dontFixRetract VersionFixer = func(_, vers string) (string, error) { + return vers, nil +} + +// Parse parses and returns a go.mod file. +// +// file is the name of the file, used in positions and errors. +// +// data is the content of the file. +// +// fix is an optional function that canonicalizes module versions. +// If fix is nil, all module versions must be canonical ([module.CanonicalVersion] +// must return the same string). +func Parse(file string, data []byte, fix VersionFixer) (*File, error) { + return parseToFile(file, data, fix, true) +} + +// ParseLax is like Parse but ignores unknown statements. +// It is used when parsing go.mod files other than the main module, +// under the theory that most statement types we add in the future will +// only apply in the main module, like exclude and replace, +// and so we get better gradual deployments if old go commands +// simply ignore those statements when found in go.mod files +// in dependencies. +func ParseLax(file string, data []byte, fix VersionFixer) (*File, error) { + return parseToFile(file, data, fix, false) +} + +func parseToFile(file string, data []byte, fix VersionFixer, strict bool) (parsed *File, err error) { + fs, err := parse(file, data) + if err != nil { + return nil, err + } + f := &File{ + Syntax: fs, + } + var errs ErrorList + + // fix versions in retract directives after the file is parsed. + // We need the module path to fix versions, and it might be at the end. + defer func() { + oldLen := len(errs) + f.fixRetract(fix, &errs) + if len(errs) > oldLen { + parsed, err = nil, errs + } + }() + + for _, x := range fs.Stmt { + switch x := x.(type) { + case *Line: + f.add(&errs, nil, x, x.Token[0], x.Token[1:], fix, strict) + + case *LineBlock: + if len(x.Token) > 1 { + if strict { + errs = append(errs, Error{ + Filename: file, + Pos: x.Start, + Err: fmt.Errorf("unknown block type: %s", strings.Join(x.Token, " ")), + }) + } + continue + } + switch x.Token[0] { + default: + if strict { + errs = append(errs, Error{ + Filename: file, + Pos: x.Start, + Err: fmt.Errorf("unknown block type: %s", strings.Join(x.Token, " ")), + }) + } + continue + case "module", "godebug", "require", "exclude", "replace", "retract", "tool": + for _, l := range x.Line { + f.add(&errs, x, l, x.Token[0], l.Token, fix, strict) + } + } + } + } + + if len(errs) > 0 { + return nil, errs + } + return f, nil +} + +var GoVersionRE = lazyregexp.New(`^([1-9][0-9]*)\.(0|[1-9][0-9]*)(\.(0|[1-9][0-9]*))?([a-z]+[0-9]+)?$`) +var laxGoVersionRE = lazyregexp.New(`^v?(([1-9][0-9]*)\.(0|[1-9][0-9]*))([^0-9].*)$`) + +// Toolchains must be named beginning with `go1`, +// like "go1.20.3" or "go1.20.3-gccgo". As a special case, "default" is also permitted. +// Note that this regexp is a much looser condition than go/version.IsValid, +// for forward compatibility. +// (This code has to be work to identify new toolchains even if we tweak the syntax in the future.) +var ToolchainRE = lazyregexp.New(`^default$|^go1($|\.)`) + +func (f *File) add(errs *ErrorList, block *LineBlock, line *Line, verb string, args []string, fix VersionFixer, strict bool) { + // If strict is false, this module is a dependency. + // We ignore all unknown directives as well as main-module-only + // directives like replace and exclude. It will work better for + // forward compatibility if we can depend on modules that have unknown + // statements (presumed relevant only when acting as the main module) + // and simply ignore those statements. + if !strict { + switch verb { + case "go", "module", "retract", "require": + // want these even for dependency go.mods + default: + return + } + } + + wrapModPathError := func(modPath string, err error) { + *errs = append(*errs, Error{ + Filename: f.Syntax.Name, + Pos: line.Start, + ModPath: modPath, + Verb: verb, + Err: err, + }) + } + wrapError := func(err error) { + *errs = append(*errs, Error{ + Filename: f.Syntax.Name, + Pos: line.Start, + Err: err, + }) + } + errorf := func(format string, args ...interface{}) { + wrapError(fmt.Errorf(format, args...)) + } + + switch verb { + default: + errorf("unknown directive: %s", verb) + + case "go": + if f.Go != nil { + errorf("repeated go statement") + return + } + if len(args) != 1 { + errorf("go directive expects exactly one argument") + return + } else if !GoVersionRE.MatchString(args[0]) { + fixed := false + if !strict { + if m := laxGoVersionRE.FindStringSubmatch(args[0]); m != nil { + args[0] = m[1] + fixed = true + } + } + if !fixed { + errorf("invalid go version '%s': must match format 1.23.0", args[0]) + return + } + } + + f.Go = &Go{Syntax: line} + f.Go.Version = args[0] + + case "toolchain": + if f.Toolchain != nil { + errorf("repeated toolchain statement") + return + } + if len(args) != 1 { + errorf("toolchain directive expects exactly one argument") + return + } else if !ToolchainRE.MatchString(args[0]) { + errorf("invalid toolchain version '%s': must match format go1.23.0 or default", args[0]) + return + } + f.Toolchain = &Toolchain{Syntax: line} + f.Toolchain.Name = args[0] + + case "module": + if f.Module != nil { + errorf("repeated module statement") + return + } + deprecated := parseDeprecation(block, line) + f.Module = &Module{ + Syntax: line, + Deprecated: deprecated, + } + if len(args) != 1 { + errorf("usage: module module/path") + return + } + s, err := parseString(&args[0]) + if err != nil { + errorf("invalid quoted string: %v", err) + return + } + f.Module.Mod = module.Version{Path: s} + + case "godebug": + if len(args) != 1 || strings.ContainsAny(args[0], "\"`',") { + errorf("usage: godebug key=value") + return + } + key, value, ok := strings.Cut(args[0], "=") + if !ok { + errorf("usage: godebug key=value") + return + } + f.Godebug = append(f.Godebug, &Godebug{ + Key: key, + Value: value, + Syntax: line, + }) + + case "require", "exclude": + if len(args) != 2 { + errorf("usage: %s module/path v1.2.3", verb) + return + } + s, err := parseString(&args[0]) + if err != nil { + errorf("invalid quoted string: %v", err) + return + } + v, err := parseVersion(verb, s, &args[1], fix) + if err != nil { + wrapError(err) + return + } + pathMajor, err := modulePathMajor(s) + if err != nil { + wrapError(err) + return + } + if err := module.CheckPathMajor(v, pathMajor); err != nil { + wrapModPathError(s, err) + return + } + if verb == "require" { + f.Require = append(f.Require, &Require{ + Mod: module.Version{Path: s, Version: v}, + Syntax: line, + Indirect: isIndirect(line), + }) + } else { + f.Exclude = append(f.Exclude, &Exclude{ + Mod: module.Version{Path: s, Version: v}, + Syntax: line, + }) + } + + case "replace": + replace, wrappederr := parseReplace(f.Syntax.Name, line, verb, args, fix) + if wrappederr != nil { + *errs = append(*errs, *wrappederr) + return + } + f.Replace = append(f.Replace, replace) + + case "retract": + rationale := parseDirectiveComment(block, line) + vi, err := parseVersionInterval(verb, "", &args, dontFixRetract) + if err != nil { + if strict { + wrapError(err) + return + } else { + // Only report errors parsing intervals in the main module. We may + // support additional syntax in the future, such as open and half-open + // intervals. Those can't be supported now, because they break the + // go.mod parser, even in lax mode. + return + } + } + if len(args) > 0 && strict { + // In the future, there may be additional information after the version. + errorf("unexpected token after version: %q", args[0]) + return + } + retract := &Retract{ + VersionInterval: vi, + Rationale: rationale, + Syntax: line, + } + f.Retract = append(f.Retract, retract) + + case "tool": + if len(args) != 1 { + errorf("tool directive expects exactly one argument") + return + } + s, err := parseString(&args[0]) + if err != nil { + errorf("invalid quoted string: %v", err) + return + } + f.Tool = append(f.Tool, &Tool{ + Path: s, + Syntax: line, + }) + } +} + +func parseReplace(filename string, line *Line, verb string, args []string, fix VersionFixer) (*Replace, *Error) { + wrapModPathError := func(modPath string, err error) *Error { + return &Error{ + Filename: filename, + Pos: line.Start, + ModPath: modPath, + Verb: verb, + Err: err, + } + } + wrapError := func(err error) *Error { + return &Error{ + Filename: filename, + Pos: line.Start, + Err: err, + } + } + errorf := func(format string, args ...interface{}) *Error { + return wrapError(fmt.Errorf(format, args...)) + } + + arrow := 2 + if len(args) >= 2 && args[1] == "=>" { + arrow = 1 + } + if len(args) < arrow+2 || len(args) > arrow+3 || args[arrow] != "=>" { + return nil, errorf("usage: %s module/path [v1.2.3] => other/module v1.4\n\t or %s module/path [v1.2.3] => ../local/directory", verb, verb) + } + s, err := parseString(&args[0]) + if err != nil { + return nil, errorf("invalid quoted string: %v", err) + } + pathMajor, err := modulePathMajor(s) + if err != nil { + return nil, wrapModPathError(s, err) + + } + var v string + if arrow == 2 { + v, err = parseVersion(verb, s, &args[1], fix) + if err != nil { + return nil, wrapError(err) + } + if err := module.CheckPathMajor(v, pathMajor); err != nil { + return nil, wrapModPathError(s, err) + } + } + ns, err := parseString(&args[arrow+1]) + if err != nil { + return nil, errorf("invalid quoted string: %v", err) + } + nv := "" + if len(args) == arrow+2 { + if !IsDirectoryPath(ns) { + if strings.Contains(ns, "@") { + return nil, errorf("replacement module must match format 'path version', not 'path@version'") + } + return nil, errorf("replacement module without version must be directory path (rooted or starting with . or ..)") + } + if filepath.Separator == '/' && strings.Contains(ns, `\`) { + return nil, errorf("replacement directory appears to be Windows path (on a non-windows system)") + } + } + if len(args) == arrow+3 { + nv, err = parseVersion(verb, ns, &args[arrow+2], fix) + if err != nil { + return nil, wrapError(err) + } + if IsDirectoryPath(ns) { + return nil, errorf("replacement module directory path %q cannot have version", ns) + } + } + return &Replace{ + Old: module.Version{Path: s, Version: v}, + New: module.Version{Path: ns, Version: nv}, + Syntax: line, + }, nil +} + +// fixRetract applies fix to each retract directive in f, appending any errors +// to errs. +// +// Most versions are fixed as we parse the file, but for retract directives, +// the relevant module path is the one specified with the module directive, +// and that might appear at the end of the file (or not at all). +func (f *File) fixRetract(fix VersionFixer, errs *ErrorList) { + if fix == nil { + return + } + path := "" + if f.Module != nil { + path = f.Module.Mod.Path + } + var r *Retract + wrapError := func(err error) { + *errs = append(*errs, Error{ + Filename: f.Syntax.Name, + Pos: r.Syntax.Start, + Err: err, + }) + } + + for _, r = range f.Retract { + if path == "" { + wrapError(errors.New("no module directive found, so retract cannot be used")) + return // only print the first one of these + } + + args := r.Syntax.Token + if args[0] == "retract" { + args = args[1:] + } + vi, err := parseVersionInterval("retract", path, &args, fix) + if err != nil { + wrapError(err) + } + r.VersionInterval = vi + } +} + +func (f *WorkFile) add(errs *ErrorList, line *Line, verb string, args []string, fix VersionFixer) { + wrapError := func(err error) { + *errs = append(*errs, Error{ + Filename: f.Syntax.Name, + Pos: line.Start, + Err: err, + }) + } + errorf := func(format string, args ...interface{}) { + wrapError(fmt.Errorf(format, args...)) + } + + switch verb { + default: + errorf("unknown directive: %s", verb) + + case "go": + if f.Go != nil { + errorf("repeated go statement") + return + } + if len(args) != 1 { + errorf("go directive expects exactly one argument") + return + } else if !GoVersionRE.MatchString(args[0]) { + errorf("invalid go version '%s': must match format 1.23.0", args[0]) + return + } + + f.Go = &Go{Syntax: line} + f.Go.Version = args[0] + + case "toolchain": + if f.Toolchain != nil { + errorf("repeated toolchain statement") + return + } + if len(args) != 1 { + errorf("toolchain directive expects exactly one argument") + return + } else if !ToolchainRE.MatchString(args[0]) { + errorf("invalid toolchain version '%s': must match format go1.23.0 or default", args[0]) + return + } + + f.Toolchain = &Toolchain{Syntax: line} + f.Toolchain.Name = args[0] + + case "godebug": + if len(args) != 1 || strings.ContainsAny(args[0], "\"`',") { + errorf("usage: godebug key=value") + return + } + key, value, ok := strings.Cut(args[0], "=") + if !ok { + errorf("usage: godebug key=value") + return + } + f.Godebug = append(f.Godebug, &Godebug{ + Key: key, + Value: value, + Syntax: line, + }) + + case "use": + if len(args) != 1 { + errorf("usage: %s local/dir", verb) + return + } + s, err := parseString(&args[0]) + if err != nil { + errorf("invalid quoted string: %v", err) + return + } + f.Use = append(f.Use, &Use{ + Path: s, + Syntax: line, + }) + + case "replace": + replace, wrappederr := parseReplace(f.Syntax.Name, line, verb, args, fix) + if wrappederr != nil { + *errs = append(*errs, *wrappederr) + return + } + f.Replace = append(f.Replace, replace) + } +} + +// IsDirectoryPath reports whether the given path should be interpreted as a directory path. +// Just like on the go command line, relative paths starting with a '.' or '..' path component +// and rooted paths are directory paths; the rest are module paths. +func IsDirectoryPath(ns string) bool { + // Because go.mod files can move from one system to another, + // we check all known path syntaxes, both Unix and Windows. + return ns == "." || strings.HasPrefix(ns, "./") || strings.HasPrefix(ns, `.\`) || + ns == ".." || strings.HasPrefix(ns, "../") || strings.HasPrefix(ns, `..\`) || + strings.HasPrefix(ns, "/") || strings.HasPrefix(ns, `\`) || + len(ns) >= 2 && ('A' <= ns[0] && ns[0] <= 'Z' || 'a' <= ns[0] && ns[0] <= 'z') && ns[1] == ':' +} + +// MustQuote reports whether s must be quoted in order to appear as +// a single token in a go.mod line. +func MustQuote(s string) bool { + for _, r := range s { + switch r { + case ' ', '"', '\'', '`': + return true + + case '(', ')', '[', ']', '{', '}', ',': + if len(s) > 1 { + return true + } + + default: + if !unicode.IsPrint(r) { + return true + } + } + } + return s == "" || strings.Contains(s, "//") || strings.Contains(s, "/*") +} + +// AutoQuote returns s or, if quoting is required for s to appear in a go.mod, +// the quotation of s. +func AutoQuote(s string) string { + if MustQuote(s) { + return strconv.Quote(s) + } + return s +} + +func parseVersionInterval(verb string, path string, args *[]string, fix VersionFixer) (VersionInterval, error) { + toks := *args + if len(toks) == 0 || toks[0] == "(" { + return VersionInterval{}, fmt.Errorf("expected '[' or version") + } + if toks[0] != "[" { + v, err := parseVersion(verb, path, &toks[0], fix) + if err != nil { + return VersionInterval{}, err + } + *args = toks[1:] + return VersionInterval{Low: v, High: v}, nil + } + toks = toks[1:] + + if len(toks) == 0 { + return VersionInterval{}, fmt.Errorf("expected version after '['") + } + low, err := parseVersion(verb, path, &toks[0], fix) + if err != nil { + return VersionInterval{}, err + } + toks = toks[1:] + + if len(toks) == 0 || toks[0] != "," { + return VersionInterval{}, fmt.Errorf("expected ',' after version") + } + toks = toks[1:] + + if len(toks) == 0 { + return VersionInterval{}, fmt.Errorf("expected version after ','") + } + high, err := parseVersion(verb, path, &toks[0], fix) + if err != nil { + return VersionInterval{}, err + } + toks = toks[1:] + + if len(toks) == 0 || toks[0] != "]" { + return VersionInterval{}, fmt.Errorf("expected ']' after version") + } + toks = toks[1:] + + *args = toks + return VersionInterval{Low: low, High: high}, nil +} + +func parseString(s *string) (string, error) { + t := *s + if strings.HasPrefix(t, `"`) { + var err error + if t, err = strconv.Unquote(t); err != nil { + return "", err + } + } else if strings.ContainsAny(t, "\"'`") { + // Other quotes are reserved both for possible future expansion + // and to avoid confusion. For example if someone types 'x' + // we want that to be a syntax error and not a literal x in literal quotation marks. + return "", fmt.Errorf("unquoted string cannot contain quote") + } + *s = AutoQuote(t) + return t, nil +} + +var deprecatedRE = lazyregexp.New(`(?s)(?:^|\n\n)Deprecated: *(.*?)(?:$|\n\n)`) + +// parseDeprecation extracts the text of comments on a "module" directive and +// extracts a deprecation message from that. +// +// A deprecation message is contained in a paragraph within a block of comments +// that starts with "Deprecated:" (case sensitive). The message runs until the +// end of the paragraph and does not include the "Deprecated:" prefix. If the +// comment block has multiple paragraphs that start with "Deprecated:", +// parseDeprecation returns the message from the first. +func parseDeprecation(block *LineBlock, line *Line) string { + text := parseDirectiveComment(block, line) + m := deprecatedRE.FindStringSubmatch(text) + if m == nil { + return "" + } + return m[1] +} + +// parseDirectiveComment extracts the text of comments on a directive. +// If the directive's line does not have comments and is part of a block that +// does have comments, the block's comments are used. +func parseDirectiveComment(block *LineBlock, line *Line) string { + comments := line.Comment() + if block != nil && len(comments.Before) == 0 && len(comments.Suffix) == 0 { + comments = block.Comment() + } + groups := [][]Comment{comments.Before, comments.Suffix} + var lines []string + for _, g := range groups { + for _, c := range g { + if !strings.HasPrefix(c.Token, "//") { + continue // blank line + } + lines = append(lines, strings.TrimSpace(strings.TrimPrefix(c.Token, "//"))) + } + } + return strings.Join(lines, "\n") +} + +type ErrorList []Error + +func (e ErrorList) Error() string { + errStrs := make([]string, len(e)) + for i, err := range e { + errStrs[i] = err.Error() + } + return strings.Join(errStrs, "\n") +} + +type Error struct { + Filename string + Pos Position + Verb string + ModPath string + Err error +} + +func (e *Error) Error() string { + var pos string + if e.Pos.LineRune > 1 { + // Don't print LineRune if it's 1 (beginning of line). + // It's always 1 except in scanner errors, which are rare. + pos = fmt.Sprintf("%s:%d:%d: ", e.Filename, e.Pos.Line, e.Pos.LineRune) + } else if e.Pos.Line > 0 { + pos = fmt.Sprintf("%s:%d: ", e.Filename, e.Pos.Line) + } else if e.Filename != "" { + pos = fmt.Sprintf("%s: ", e.Filename) + } + + var directive string + if e.ModPath != "" { + directive = fmt.Sprintf("%s %s: ", e.Verb, e.ModPath) + } else if e.Verb != "" { + directive = fmt.Sprintf("%s: ", e.Verb) + } + + return pos + directive + e.Err.Error() +} + +func (e *Error) Unwrap() error { return e.Err } + +func parseVersion(verb string, path string, s *string, fix VersionFixer) (string, error) { + t, err := parseString(s) + if err != nil { + return "", &Error{ + Verb: verb, + ModPath: path, + Err: &module.InvalidVersionError{ + Version: *s, + Err: err, + }, + } + } + if fix != nil { + fixed, err := fix(path, t) + if err != nil { + if err, ok := err.(*module.ModuleError); ok { + return "", &Error{ + Verb: verb, + ModPath: path, + Err: err.Err, + } + } + return "", err + } + t = fixed + } else { + cv := module.CanonicalVersion(t) + if cv == "" { + return "", &Error{ + Verb: verb, + ModPath: path, + Err: &module.InvalidVersionError{ + Version: t, + Err: errors.New("must be of the form v1.2.3"), + }, + } + } + t = cv + } + *s = t + return *s, nil +} + +func modulePathMajor(path string) (string, error) { + _, major, ok := module.SplitPathVersion(path) + if !ok { + return "", fmt.Errorf("invalid module path") + } + return major, nil +} + +func (f *File) Format() ([]byte, error) { + return Format(f.Syntax), nil +} + +// Cleanup cleans up the file f after any edit operations. +// To avoid quadratic behavior, modifications like [File.DropRequire] +// clear the entry but do not remove it from the slice. +// Cleanup cleans out all the cleared entries. +func (f *File) Cleanup() { + w := 0 + for _, g := range f.Godebug { + if g.Key != "" { + f.Godebug[w] = g + w++ + } + } + f.Godebug = f.Godebug[:w] + + w = 0 + for _, r := range f.Require { + if r.Mod.Path != "" { + f.Require[w] = r + w++ + } + } + f.Require = f.Require[:w] + + w = 0 + for _, x := range f.Exclude { + if x.Mod.Path != "" { + f.Exclude[w] = x + w++ + } + } + f.Exclude = f.Exclude[:w] + + w = 0 + for _, r := range f.Replace { + if r.Old.Path != "" { + f.Replace[w] = r + w++ + } + } + f.Replace = f.Replace[:w] + + w = 0 + for _, r := range f.Retract { + if r.Low != "" || r.High != "" { + f.Retract[w] = r + w++ + } + } + f.Retract = f.Retract[:w] + + f.Syntax.Cleanup() +} + +func (f *File) AddGoStmt(version string) error { + if !GoVersionRE.MatchString(version) { + return fmt.Errorf("invalid language version %q", version) + } + if f.Go == nil { + var hint Expr + if f.Module != nil && f.Module.Syntax != nil { + hint = f.Module.Syntax + } else if f.Syntax == nil { + f.Syntax = new(FileSyntax) + } + f.Go = &Go{ + Version: version, + Syntax: f.Syntax.addLine(hint, "go", version), + } + } else { + f.Go.Version = version + f.Syntax.updateLine(f.Go.Syntax, "go", version) + } + return nil +} + +// DropGoStmt deletes the go statement from the file. +func (f *File) DropGoStmt() { + if f.Go != nil { + f.Go.Syntax.markRemoved() + f.Go = nil + } +} + +// DropToolchainStmt deletes the toolchain statement from the file. +func (f *File) DropToolchainStmt() { + if f.Toolchain != nil { + f.Toolchain.Syntax.markRemoved() + f.Toolchain = nil + } +} + +func (f *File) AddToolchainStmt(name string) error { + if !ToolchainRE.MatchString(name) { + return fmt.Errorf("invalid toolchain name %q", name) + } + if f.Toolchain == nil { + var hint Expr + if f.Go != nil && f.Go.Syntax != nil { + hint = f.Go.Syntax + } else if f.Module != nil && f.Module.Syntax != nil { + hint = f.Module.Syntax + } + f.Toolchain = &Toolchain{ + Name: name, + Syntax: f.Syntax.addLine(hint, "toolchain", name), + } + } else { + f.Toolchain.Name = name + f.Syntax.updateLine(f.Toolchain.Syntax, "toolchain", name) + } + return nil +} + +// AddGodebug sets the first godebug line for key to value, +// preserving any existing comments for that line and removing all +// other godebug lines for key. +// +// If no line currently exists for key, AddGodebug adds a new line +// at the end of the last godebug block. +func (f *File) AddGodebug(key, value string) error { + need := true + for _, g := range f.Godebug { + if g.Key == key { + if need { + g.Value = value + f.Syntax.updateLine(g.Syntax, "godebug", key+"="+value) + need = false + } else { + g.Syntax.markRemoved() + *g = Godebug{} + } + } + } + + if need { + f.addNewGodebug(key, value) + } + return nil +} + +// addNewGodebug adds a new godebug key=value line at the end +// of the last godebug block, regardless of any existing godebug lines for key. +func (f *File) addNewGodebug(key, value string) { + line := f.Syntax.addLine(nil, "godebug", key+"="+value) + g := &Godebug{ + Key: key, + Value: value, + Syntax: line, + } + f.Godebug = append(f.Godebug, g) +} + +// AddRequire sets the first require line for path to version vers, +// preserving any existing comments for that line and removing all +// other lines for path. +// +// If no line currently exists for path, AddRequire adds a new line +// at the end of the last require block. +func (f *File) AddRequire(path, vers string) error { + need := true + for _, r := range f.Require { + if r.Mod.Path == path { + if need { + r.Mod.Version = vers + f.Syntax.updateLine(r.Syntax, "require", AutoQuote(path), vers) + need = false + } else { + r.Syntax.markRemoved() + *r = Require{} + } + } + } + + if need { + f.AddNewRequire(path, vers, false) + } + return nil +} + +// AddNewRequire adds a new require line for path at version vers at the end of +// the last require block, regardless of any existing require lines for path. +func (f *File) AddNewRequire(path, vers string, indirect bool) { + line := f.Syntax.addLine(nil, "require", AutoQuote(path), vers) + r := &Require{ + Mod: module.Version{Path: path, Version: vers}, + Syntax: line, + } + r.setIndirect(indirect) + f.Require = append(f.Require, r) +} + +// SetRequire updates the requirements of f to contain exactly req, preserving +// the existing block structure and line comment contents (except for 'indirect' +// markings) for the first requirement on each named module path. +// +// The Syntax field is ignored for the requirements in req. +// +// Any requirements not already present in the file are added to the block +// containing the last require line. +// +// The requirements in req must specify at most one distinct version for each +// module path. +// +// If any existing requirements may be removed, the caller should call +// [File.Cleanup] after all edits are complete. +func (f *File) SetRequire(req []*Require) { + type elem struct { + version string + indirect bool + } + need := make(map[string]elem) + for _, r := range req { + if prev, dup := need[r.Mod.Path]; dup && prev.version != r.Mod.Version { + panic(fmt.Errorf("SetRequire called with conflicting versions for path %s (%s and %s)", r.Mod.Path, prev.version, r.Mod.Version)) + } + need[r.Mod.Path] = elem{r.Mod.Version, r.Indirect} + } + + // Update or delete the existing Require entries to preserve + // only the first for each module path in req. + for _, r := range f.Require { + e, ok := need[r.Mod.Path] + if ok { + r.setVersion(e.version) + r.setIndirect(e.indirect) + } else { + r.markRemoved() + } + delete(need, r.Mod.Path) + } + + // Add new entries in the last block of the file for any paths that weren't + // already present. + // + // This step is nondeterministic, but the final result will be deterministic + // because we will sort the block. + for path, e := range need { + f.AddNewRequire(path, e.version, e.indirect) + } + + f.SortBlocks() +} + +// SetRequireSeparateIndirect updates the requirements of f to contain the given +// requirements. Comment contents (except for 'indirect' markings) are retained +// from the first existing requirement for each module path. Like SetRequire, +// SetRequireSeparateIndirect adds requirements for new paths in req, +// updates the version and "// indirect" comment on existing requirements, +// and deletes requirements on paths not in req. Existing duplicate requirements +// are deleted. +// +// As its name suggests, SetRequireSeparateIndirect puts direct and indirect +// requirements into two separate blocks, one containing only direct +// requirements, and the other containing only indirect requirements. +// SetRequireSeparateIndirect may move requirements between these two blocks +// when their indirect markings change. However, SetRequireSeparateIndirect +// won't move requirements from other blocks, especially blocks with comments. +// +// If the file initially has one uncommented block of requirements, +// SetRequireSeparateIndirect will split it into a direct-only and indirect-only +// block. This aids in the transition to separate blocks. +func (f *File) SetRequireSeparateIndirect(req []*Require) { + // hasComments returns whether a line or block has comments + // other than "indirect". + hasComments := func(c Comments) bool { + return len(c.Before) > 0 || len(c.After) > 0 || len(c.Suffix) > 1 || + (len(c.Suffix) == 1 && + strings.TrimSpace(strings.TrimPrefix(c.Suffix[0].Token, string(slashSlash))) != "indirect") + } + + // moveReq adds r to block. If r was in another block, moveReq deletes + // it from that block and transfers its comments. + moveReq := func(r *Require, block *LineBlock) { + var line *Line + if r.Syntax == nil { + line = &Line{Token: []string{AutoQuote(r.Mod.Path), r.Mod.Version}} + r.Syntax = line + if r.Indirect { + r.setIndirect(true) + } + } else { + line = new(Line) + *line = *r.Syntax + if !line.InBlock && len(line.Token) > 0 && line.Token[0] == "require" { + line.Token = line.Token[1:] + } + r.Syntax.Token = nil // Cleanup will delete the old line. + r.Syntax = line + } + line.InBlock = true + block.Line = append(block.Line, line) + } + + // Examine existing require lines and blocks. + var ( + // We may insert new requirements into the last uncommented + // direct-only and indirect-only blocks. We may also move requirements + // to the opposite block if their indirect markings change. + lastDirectIndex = -1 + lastIndirectIndex = -1 + + // If there are no direct-only or indirect-only blocks, a new block may + // be inserted after the last require line or block. + lastRequireIndex = -1 + + // If there's only one require line or block, and it's uncommented, + // we'll move its requirements to the direct-only or indirect-only blocks. + requireLineOrBlockCount = 0 + + // Track the block each requirement belongs to (if any) so we can + // move them later. + lineToBlock = make(map[*Line]*LineBlock) + ) + for i, stmt := range f.Syntax.Stmt { + switch stmt := stmt.(type) { + case *Line: + if len(stmt.Token) == 0 || stmt.Token[0] != "require" { + continue + } + lastRequireIndex = i + requireLineOrBlockCount++ + if !hasComments(stmt.Comments) { + if isIndirect(stmt) { + lastIndirectIndex = i + } else { + lastDirectIndex = i + } + } + + case *LineBlock: + if len(stmt.Token) == 0 || stmt.Token[0] != "require" { + continue + } + lastRequireIndex = i + requireLineOrBlockCount++ + allDirect := len(stmt.Line) > 0 && !hasComments(stmt.Comments) + allIndirect := len(stmt.Line) > 0 && !hasComments(stmt.Comments) + for _, line := range stmt.Line { + lineToBlock[line] = stmt + if hasComments(line.Comments) { + allDirect = false + allIndirect = false + } else if isIndirect(line) { + allDirect = false + } else { + allIndirect = false + } + } + if allDirect { + lastDirectIndex = i + } + if allIndirect { + lastIndirectIndex = i + } + } + } + + oneFlatUncommentedBlock := requireLineOrBlockCount == 1 && + !hasComments(*f.Syntax.Stmt[lastRequireIndex].Comment()) + + // Create direct and indirect blocks if needed. Convert lines into blocks + // if needed. If we end up with an empty block or a one-line block, + // Cleanup will delete it or convert it to a line later. + insertBlock := func(i int) *LineBlock { + block := &LineBlock{Token: []string{"require"}} + f.Syntax.Stmt = append(f.Syntax.Stmt, nil) + copy(f.Syntax.Stmt[i+1:], f.Syntax.Stmt[i:]) + f.Syntax.Stmt[i] = block + return block + } + + ensureBlock := func(i int) *LineBlock { + switch stmt := f.Syntax.Stmt[i].(type) { + case *LineBlock: + return stmt + case *Line: + block := &LineBlock{ + Token: []string{"require"}, + Line: []*Line{stmt}, + } + stmt.Token = stmt.Token[1:] // remove "require" + stmt.InBlock = true + f.Syntax.Stmt[i] = block + return block + default: + panic(fmt.Sprintf("unexpected statement: %v", stmt)) + } + } + + var lastDirectBlock *LineBlock + if lastDirectIndex < 0 { + if lastIndirectIndex >= 0 { + lastDirectIndex = lastIndirectIndex + lastIndirectIndex++ + } else if lastRequireIndex >= 0 { + lastDirectIndex = lastRequireIndex + 1 + } else { + lastDirectIndex = len(f.Syntax.Stmt) + } + lastDirectBlock = insertBlock(lastDirectIndex) + } else { + lastDirectBlock = ensureBlock(lastDirectIndex) + } + + var lastIndirectBlock *LineBlock + if lastIndirectIndex < 0 { + lastIndirectIndex = lastDirectIndex + 1 + lastIndirectBlock = insertBlock(lastIndirectIndex) + } else { + lastIndirectBlock = ensureBlock(lastIndirectIndex) + } + + // Delete requirements we don't want anymore. + // Update versions and indirect comments on requirements we want to keep. + // If a requirement is in last{Direct,Indirect}Block with the wrong + // indirect marking after this, or if the requirement is in an single + // uncommented mixed block (oneFlatUncommentedBlock), move it to the + // correct block. + // + // Some blocks may be empty after this. Cleanup will remove them. + need := make(map[string]*Require) + for _, r := range req { + need[r.Mod.Path] = r + } + have := make(map[string]*Require) + for _, r := range f.Require { + path := r.Mod.Path + if need[path] == nil || have[path] != nil { + // Requirement not needed, or duplicate requirement. Delete. + r.markRemoved() + continue + } + have[r.Mod.Path] = r + r.setVersion(need[path].Mod.Version) + r.setIndirect(need[path].Indirect) + if need[path].Indirect && + (oneFlatUncommentedBlock || lineToBlock[r.Syntax] == lastDirectBlock) { + moveReq(r, lastIndirectBlock) + } else if !need[path].Indirect && + (oneFlatUncommentedBlock || lineToBlock[r.Syntax] == lastIndirectBlock) { + moveReq(r, lastDirectBlock) + } + } + + // Add new requirements. + for path, r := range need { + if have[path] == nil { + if r.Indirect { + moveReq(r, lastIndirectBlock) + } else { + moveReq(r, lastDirectBlock) + } + f.Require = append(f.Require, r) + } + } + + f.SortBlocks() +} + +func (f *File) DropGodebug(key string) error { + for _, g := range f.Godebug { + if g.Key == key { + g.Syntax.markRemoved() + *g = Godebug{} + } + } + return nil +} + +func (f *File) DropRequire(path string) error { + for _, r := range f.Require { + if r.Mod.Path == path { + r.Syntax.markRemoved() + *r = Require{} + } + } + return nil +} + +// AddExclude adds a exclude statement to the mod file. Errors if the provided +// version is not a canonical version string +func (f *File) AddExclude(path, vers string) error { + if err := checkCanonicalVersion(path, vers); err != nil { + return err + } + + var hint *Line + for _, x := range f.Exclude { + if x.Mod.Path == path && x.Mod.Version == vers { + return nil + } + if x.Mod.Path == path { + hint = x.Syntax + } + } + + f.Exclude = append(f.Exclude, &Exclude{Mod: module.Version{Path: path, Version: vers}, Syntax: f.Syntax.addLine(hint, "exclude", AutoQuote(path), vers)}) + return nil +} + +func (f *File) DropExclude(path, vers string) error { + for _, x := range f.Exclude { + if x.Mod.Path == path && x.Mod.Version == vers { + x.Syntax.markRemoved() + *x = Exclude{} + } + } + return nil +} + +func (f *File) AddReplace(oldPath, oldVers, newPath, newVers string) error { + return addReplace(f.Syntax, &f.Replace, oldPath, oldVers, newPath, newVers) +} + +func addReplace(syntax *FileSyntax, replace *[]*Replace, oldPath, oldVers, newPath, newVers string) error { + need := true + old := module.Version{Path: oldPath, Version: oldVers} + new := module.Version{Path: newPath, Version: newVers} + tokens := []string{"replace", AutoQuote(oldPath)} + if oldVers != "" { + tokens = append(tokens, oldVers) + } + tokens = append(tokens, "=>", AutoQuote(newPath)) + if newVers != "" { + tokens = append(tokens, newVers) + } + + var hint *Line + for _, r := range *replace { + if r.Old.Path == oldPath && (oldVers == "" || r.Old.Version == oldVers) { + if need { + // Found replacement for old; update to use new. + r.New = new + syntax.updateLine(r.Syntax, tokens...) + need = false + continue + } + // Already added; delete other replacements for same. + r.Syntax.markRemoved() + *r = Replace{} + } + if r.Old.Path == oldPath { + hint = r.Syntax + } + } + if need { + *replace = append(*replace, &Replace{Old: old, New: new, Syntax: syntax.addLine(hint, tokens...)}) + } + return nil +} + +func (f *File) DropReplace(oldPath, oldVers string) error { + for _, r := range f.Replace { + if r.Old.Path == oldPath && r.Old.Version == oldVers { + r.Syntax.markRemoved() + *r = Replace{} + } + } + return nil +} + +// AddRetract adds a retract statement to the mod file. Errors if the provided +// version interval does not consist of canonical version strings +func (f *File) AddRetract(vi VersionInterval, rationale string) error { + var path string + if f.Module != nil { + path = f.Module.Mod.Path + } + if err := checkCanonicalVersion(path, vi.High); err != nil { + return err + } + if err := checkCanonicalVersion(path, vi.Low); err != nil { + return err + } + + r := &Retract{ + VersionInterval: vi, + } + if vi.Low == vi.High { + r.Syntax = f.Syntax.addLine(nil, "retract", AutoQuote(vi.Low)) + } else { + r.Syntax = f.Syntax.addLine(nil, "retract", "[", AutoQuote(vi.Low), ",", AutoQuote(vi.High), "]") + } + if rationale != "" { + for _, line := range strings.Split(rationale, "\n") { + com := Comment{Token: "// " + line} + r.Syntax.Comment().Before = append(r.Syntax.Comment().Before, com) + } + } + return nil +} + +func (f *File) DropRetract(vi VersionInterval) error { + for _, r := range f.Retract { + if r.VersionInterval == vi { + r.Syntax.markRemoved() + *r = Retract{} + } + } + return nil +} + +// AddTool adds a new tool directive with the given path. +// It does nothing if the tool line already exists. +func (f *File) AddTool(path string) error { + for _, t := range f.Tool { + if t.Path == path { + return nil + } + } + + f.Tool = append(f.Tool, &Tool{ + Path: path, + Syntax: f.Syntax.addLine(nil, "tool", path), + }) + + f.SortBlocks() + return nil +} + +// RemoveTool removes a tool directive with the given path. +// It does nothing if no such tool directive exists. +func (f *File) DropTool(path string) error { + for _, t := range f.Tool { + if t.Path == path { + t.Syntax.markRemoved() + *t = Tool{} + } + } + return nil +} + +func (f *File) SortBlocks() { + f.removeDups() // otherwise sorting is unsafe + + // semanticSortForExcludeVersionV is the Go version (plus leading "v") at which + // lines in exclude blocks start to use semantic sort instead of lexicographic sort. + // See go.dev/issue/60028. + const semanticSortForExcludeVersionV = "v1.21" + useSemanticSortForExclude := f.Go != nil && semver.Compare("v"+f.Go.Version, semanticSortForExcludeVersionV) >= 0 + + for _, stmt := range f.Syntax.Stmt { + block, ok := stmt.(*LineBlock) + if !ok { + continue + } + less := lineLess + if block.Token[0] == "exclude" && useSemanticSortForExclude { + less = lineExcludeLess + } else if block.Token[0] == "retract" { + less = lineRetractLess + } + sort.SliceStable(block.Line, func(i, j int) bool { + return less(block.Line[i], block.Line[j]) + }) + } +} + +// removeDups removes duplicate exclude, replace and tool directives. +// +// Earlier exclude and tool directives take priority. +// +// Later replace directives take priority. +// +// require directives are not de-duplicated. That's left up to higher-level +// logic (MVS). +// +// retract directives are not de-duplicated since comments are +// meaningful, and versions may be retracted multiple times. +func (f *File) removeDups() { + removeDups(f.Syntax, &f.Exclude, &f.Replace, &f.Tool) +} + +func removeDups(syntax *FileSyntax, exclude *[]*Exclude, replace *[]*Replace, tool *[]*Tool) { + kill := make(map[*Line]bool) + + // Remove duplicate excludes. + if exclude != nil { + haveExclude := make(map[module.Version]bool) + for _, x := range *exclude { + if haveExclude[x.Mod] { + kill[x.Syntax] = true + continue + } + haveExclude[x.Mod] = true + } + var excl []*Exclude + for _, x := range *exclude { + if !kill[x.Syntax] { + excl = append(excl, x) + } + } + *exclude = excl + } + + // Remove duplicate replacements. + // Later replacements take priority over earlier ones. + haveReplace := make(map[module.Version]bool) + for i := len(*replace) - 1; i >= 0; i-- { + x := (*replace)[i] + if haveReplace[x.Old] { + kill[x.Syntax] = true + continue + } + haveReplace[x.Old] = true + } + var repl []*Replace + for _, x := range *replace { + if !kill[x.Syntax] { + repl = append(repl, x) + } + } + *replace = repl + + if tool != nil { + haveTool := make(map[string]bool) + for _, t := range *tool { + if haveTool[t.Path] { + kill[t.Syntax] = true + continue + } + haveTool[t.Path] = true + } + var newTool []*Tool + for _, t := range *tool { + if !kill[t.Syntax] { + newTool = append(newTool, t) + } + } + *tool = newTool + } + + // Duplicate require and retract directives are not removed. + + // Drop killed statements from the syntax tree. + var stmts []Expr + for _, stmt := range syntax.Stmt { + switch stmt := stmt.(type) { + case *Line: + if kill[stmt] { + continue + } + case *LineBlock: + var lines []*Line + for _, line := range stmt.Line { + if !kill[line] { + lines = append(lines, line) + } + } + stmt.Line = lines + if len(lines) == 0 { + continue + } + } + stmts = append(stmts, stmt) + } + syntax.Stmt = stmts +} + +// lineLess returns whether li should be sorted before lj. It sorts +// lexicographically without assigning any special meaning to tokens. +func lineLess(li, lj *Line) bool { + for k := 0; k < len(li.Token) && k < len(lj.Token); k++ { + if li.Token[k] != lj.Token[k] { + return li.Token[k] < lj.Token[k] + } + } + return len(li.Token) < len(lj.Token) +} + +// lineExcludeLess reports whether li should be sorted before lj for lines in +// an "exclude" block. +func lineExcludeLess(li, lj *Line) bool { + if len(li.Token) != 2 || len(lj.Token) != 2 { + // Not a known exclude specification. + // Fall back to sorting lexicographically. + return lineLess(li, lj) + } + // An exclude specification has two tokens: ModulePath and Version. + // Compare module path by string order and version by semver rules. + if pi, pj := li.Token[0], lj.Token[0]; pi != pj { + return pi < pj + } + return semver.Compare(li.Token[1], lj.Token[1]) < 0 +} + +// lineRetractLess returns whether li should be sorted before lj for lines in +// a "retract" block. It treats each line as a version interval. Single versions +// are compared as if they were intervals with the same low and high version. +// Intervals are sorted in descending order, first by low version, then by +// high version, using semver.Compare. +func lineRetractLess(li, lj *Line) bool { + interval := func(l *Line) VersionInterval { + if len(l.Token) == 1 { + return VersionInterval{Low: l.Token[0], High: l.Token[0]} + } else if len(l.Token) == 5 && l.Token[0] == "[" && l.Token[2] == "," && l.Token[4] == "]" { + return VersionInterval{Low: l.Token[1], High: l.Token[3]} + } else { + // Line in unknown format. Treat as an invalid version. + return VersionInterval{} + } + } + vii := interval(li) + vij := interval(lj) + if cmp := semver.Compare(vii.Low, vij.Low); cmp != 0 { + return cmp > 0 + } + return semver.Compare(vii.High, vij.High) > 0 +} + +// checkCanonicalVersion returns a non-nil error if vers is not a canonical +// version string or does not match the major version of path. +// +// If path is non-empty, the error text suggests a format with a major version +// corresponding to the path. +func checkCanonicalVersion(path, vers string) error { + _, pathMajor, pathMajorOk := module.SplitPathVersion(path) + + if vers == "" || vers != module.CanonicalVersion(vers) { + if pathMajor == "" { + return &module.InvalidVersionError{ + Version: vers, + Err: fmt.Errorf("must be of the form v1.2.3"), + } + } + return &module.InvalidVersionError{ + Version: vers, + Err: fmt.Errorf("must be of the form %s.2.3", module.PathMajorPrefix(pathMajor)), + } + } + + if pathMajorOk { + if err := module.CheckPathMajor(vers, pathMajor); err != nil { + if pathMajor == "" { + // In this context, the user probably wrote "v2.3.4" when they meant + // "v2.3.4+incompatible". Suggest that instead of "v0 or v1". + return &module.InvalidVersionError{ + Version: vers, + Err: fmt.Errorf("should be %s+incompatible (or module %s/%v)", vers, path, semver.Major(vers)), + } + } + return err + } + } + + return nil +} diff --git a/vendor/golang.org/x/mod/modfile/work.go b/vendor/golang.org/x/mod/modfile/work.go new file mode 100644 index 0000000..5387d0c --- /dev/null +++ b/vendor/golang.org/x/mod/modfile/work.go @@ -0,0 +1,335 @@ +// Copyright 2021 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package modfile + +import ( + "fmt" + "sort" + "strings" +) + +// A WorkFile is the parsed, interpreted form of a go.work file. +type WorkFile struct { + Go *Go + Toolchain *Toolchain + Godebug []*Godebug + Use []*Use + Replace []*Replace + + Syntax *FileSyntax +} + +// A Use is a single directory statement. +type Use struct { + Path string // Use path of module. + ModulePath string // Module path in the comment. + Syntax *Line +} + +// ParseWork parses and returns a go.work file. +// +// file is the name of the file, used in positions and errors. +// +// data is the content of the file. +// +// fix is an optional function that canonicalizes module versions. +// If fix is nil, all module versions must be canonical ([module.CanonicalVersion] +// must return the same string). +func ParseWork(file string, data []byte, fix VersionFixer) (*WorkFile, error) { + fs, err := parse(file, data) + if err != nil { + return nil, err + } + f := &WorkFile{ + Syntax: fs, + } + var errs ErrorList + + for _, x := range fs.Stmt { + switch x := x.(type) { + case *Line: + f.add(&errs, x, x.Token[0], x.Token[1:], fix) + + case *LineBlock: + if len(x.Token) > 1 { + errs = append(errs, Error{ + Filename: file, + Pos: x.Start, + Err: fmt.Errorf("unknown block type: %s", strings.Join(x.Token, " ")), + }) + continue + } + switch x.Token[0] { + default: + errs = append(errs, Error{ + Filename: file, + Pos: x.Start, + Err: fmt.Errorf("unknown block type: %s", strings.Join(x.Token, " ")), + }) + continue + case "godebug", "use", "replace": + for _, l := range x.Line { + f.add(&errs, l, x.Token[0], l.Token, fix) + } + } + } + } + + if len(errs) > 0 { + return nil, errs + } + return f, nil +} + +// Cleanup cleans up the file f after any edit operations. +// To avoid quadratic behavior, modifications like [WorkFile.DropRequire] +// clear the entry but do not remove it from the slice. +// Cleanup cleans out all the cleared entries. +func (f *WorkFile) Cleanup() { + w := 0 + for _, r := range f.Use { + if r.Path != "" { + f.Use[w] = r + w++ + } + } + f.Use = f.Use[:w] + + w = 0 + for _, r := range f.Replace { + if r.Old.Path != "" { + f.Replace[w] = r + w++ + } + } + f.Replace = f.Replace[:w] + + f.Syntax.Cleanup() +} + +func (f *WorkFile) AddGoStmt(version string) error { + if !GoVersionRE.MatchString(version) { + return fmt.Errorf("invalid language version %q", version) + } + if f.Go == nil { + stmt := &Line{Token: []string{"go", version}} + f.Go = &Go{ + Version: version, + Syntax: stmt, + } + // Find the first non-comment-only block and add + // the go statement before it. That will keep file comments at the top. + i := 0 + for i = 0; i < len(f.Syntax.Stmt); i++ { + if _, ok := f.Syntax.Stmt[i].(*CommentBlock); !ok { + break + } + } + f.Syntax.Stmt = append(append(f.Syntax.Stmt[:i:i], stmt), f.Syntax.Stmt[i:]...) + } else { + f.Go.Version = version + f.Syntax.updateLine(f.Go.Syntax, "go", version) + } + return nil +} + +func (f *WorkFile) AddToolchainStmt(name string) error { + if !ToolchainRE.MatchString(name) { + return fmt.Errorf("invalid toolchain name %q", name) + } + if f.Toolchain == nil { + stmt := &Line{Token: []string{"toolchain", name}} + f.Toolchain = &Toolchain{ + Name: name, + Syntax: stmt, + } + // Find the go line and add the toolchain line after it. + // Or else find the first non-comment-only block and add + // the toolchain line before it. That will keep file comments at the top. + i := 0 + for i = 0; i < len(f.Syntax.Stmt); i++ { + if line, ok := f.Syntax.Stmt[i].(*Line); ok && len(line.Token) > 0 && line.Token[0] == "go" { + i++ + goto Found + } + } + for i = 0; i < len(f.Syntax.Stmt); i++ { + if _, ok := f.Syntax.Stmt[i].(*CommentBlock); !ok { + break + } + } + Found: + f.Syntax.Stmt = append(append(f.Syntax.Stmt[:i:i], stmt), f.Syntax.Stmt[i:]...) + } else { + f.Toolchain.Name = name + f.Syntax.updateLine(f.Toolchain.Syntax, "toolchain", name) + } + return nil +} + +// DropGoStmt deletes the go statement from the file. +func (f *WorkFile) DropGoStmt() { + if f.Go != nil { + f.Go.Syntax.markRemoved() + f.Go = nil + } +} + +// DropToolchainStmt deletes the toolchain statement from the file. +func (f *WorkFile) DropToolchainStmt() { + if f.Toolchain != nil { + f.Toolchain.Syntax.markRemoved() + f.Toolchain = nil + } +} + +// AddGodebug sets the first godebug line for key to value, +// preserving any existing comments for that line and removing all +// other godebug lines for key. +// +// If no line currently exists for key, AddGodebug adds a new line +// at the end of the last godebug block. +func (f *WorkFile) AddGodebug(key, value string) error { + need := true + for _, g := range f.Godebug { + if g.Key == key { + if need { + g.Value = value + f.Syntax.updateLine(g.Syntax, "godebug", key+"="+value) + need = false + } else { + g.Syntax.markRemoved() + *g = Godebug{} + } + } + } + + if need { + f.addNewGodebug(key, value) + } + return nil +} + +// addNewGodebug adds a new godebug key=value line at the end +// of the last godebug block, regardless of any existing godebug lines for key. +func (f *WorkFile) addNewGodebug(key, value string) { + line := f.Syntax.addLine(nil, "godebug", key+"="+value) + g := &Godebug{ + Key: key, + Value: value, + Syntax: line, + } + f.Godebug = append(f.Godebug, g) +} + +func (f *WorkFile) DropGodebug(key string) error { + for _, g := range f.Godebug { + if g.Key == key { + g.Syntax.markRemoved() + *g = Godebug{} + } + } + return nil +} + +func (f *WorkFile) AddUse(diskPath, modulePath string) error { + need := true + for _, d := range f.Use { + if d.Path == diskPath { + if need { + d.ModulePath = modulePath + f.Syntax.updateLine(d.Syntax, "use", AutoQuote(diskPath)) + need = false + } else { + d.Syntax.markRemoved() + *d = Use{} + } + } + } + + if need { + f.AddNewUse(diskPath, modulePath) + } + return nil +} + +func (f *WorkFile) AddNewUse(diskPath, modulePath string) { + line := f.Syntax.addLine(nil, "use", AutoQuote(diskPath)) + f.Use = append(f.Use, &Use{Path: diskPath, ModulePath: modulePath, Syntax: line}) +} + +func (f *WorkFile) SetUse(dirs []*Use) { + need := make(map[string]string) + for _, d := range dirs { + need[d.Path] = d.ModulePath + } + + for _, d := range f.Use { + if modulePath, ok := need[d.Path]; ok { + d.ModulePath = modulePath + } else { + d.Syntax.markRemoved() + *d = Use{} + } + } + + // TODO(#45713): Add module path to comment. + + for diskPath, modulePath := range need { + f.AddNewUse(diskPath, modulePath) + } + f.SortBlocks() +} + +func (f *WorkFile) DropUse(path string) error { + for _, d := range f.Use { + if d.Path == path { + d.Syntax.markRemoved() + *d = Use{} + } + } + return nil +} + +func (f *WorkFile) AddReplace(oldPath, oldVers, newPath, newVers string) error { + return addReplace(f.Syntax, &f.Replace, oldPath, oldVers, newPath, newVers) +} + +func (f *WorkFile) DropReplace(oldPath, oldVers string) error { + for _, r := range f.Replace { + if r.Old.Path == oldPath && r.Old.Version == oldVers { + r.Syntax.markRemoved() + *r = Replace{} + } + } + return nil +} + +func (f *WorkFile) SortBlocks() { + f.removeDups() // otherwise sorting is unsafe + + for _, stmt := range f.Syntax.Stmt { + block, ok := stmt.(*LineBlock) + if !ok { + continue + } + sort.SliceStable(block.Line, func(i, j int) bool { + return lineLess(block.Line[i], block.Line[j]) + }) + } +} + +// removeDups removes duplicate replace directives. +// +// Later replace directives take priority. +// +// require directives are not de-duplicated. That's left up to higher-level +// logic (MVS). +// +// retract directives are not de-duplicated since comments are +// meaningful, and versions may be retracted multiple times. +func (f *WorkFile) removeDups() { + removeDups(f.Syntax, nil, &f.Replace, nil) +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 59b9b88..590d8c6 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -17,7 +17,7 @@ github.com/dprotaso/go-yit ## explicit; go 1.17 github.com/fsnotify/fsnotify github.com/fsnotify/fsnotify/internal -# github.com/getkin/kin-openapi v0.131.0 +# github.com/getkin/kin-openapi v0.132.0 ## explicit; go 1.22.5 github.com/getkin/kin-openapi/openapi3 # github.com/go-openapi/jsonpointer v0.21.0 @@ -91,8 +91,8 @@ github.com/mohae/deepcopy # github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 ## explicit github.com/munnerz/goautoneg -# github.com/oapi-codegen/oapi-codegen/v2 v2.4.1 -## explicit; go 1.21.0 +# github.com/oapi-codegen/oapi-codegen/v2 v2.5.0 +## explicit; go 1.22.5 github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen github.com/oapi-codegen/oapi-codegen/v2/pkg/util @@ -152,8 +152,13 @@ github.com/sourcegraph/conc github.com/sourcegraph/conc/internal/multierror github.com/sourcegraph/conc/iter github.com/sourcegraph/conc/panics -# github.com/speakeasy-api/openapi-overlay v0.9.0 -## explicit; go 1.21.0 +# github.com/speakeasy-api/jsonpath v0.6.0 +## explicit; go 1.22 +github.com/speakeasy-api/jsonpath/pkg/jsonpath +github.com/speakeasy-api/jsonpath/pkg/jsonpath/config +github.com/speakeasy-api/jsonpath/pkg/jsonpath/token +# github.com/speakeasy-api/openapi-overlay v0.10.2 +## explicit; go 1.22 github.com/speakeasy-api/openapi-overlay/pkg/loader github.com/speakeasy-api/openapi-overlay/pkg/overlay # github.com/spf13/afero v1.12.0 @@ -208,6 +213,7 @@ go.uber.org/multierr # golang.org/x/mod v0.23.0 ## explicit; go 1.22.0 golang.org/x/mod/internal/lazyregexp +golang.org/x/mod/modfile golang.org/x/mod/module golang.org/x/mod/semver # golang.org/x/sync v0.11.0