From 0df8497a2ab3c9ee01a6f14a82b698570e186cfb Mon Sep 17 00:00:00 2001 From: Mike Zorn Date: Mon, 24 Jun 2024 09:30:11 -0700 Subject: [PATCH 01/79] Initial API spec --- internal/dev_server/overrides.yaml | 101 +++++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 internal/dev_server/overrides.yaml diff --git a/internal/dev_server/overrides.yaml b/internal/dev_server/overrides.yaml new file mode 100644 index 00000000..50bf3b1b --- /dev/null +++ b/internal/dev_server/overrides.yaml @@ -0,0 +1,101 @@ +openapi: 3.0.3 +info: + title: LaunchDarkly Dev Server + description: | + LaunchDarkly Dev Server provides a simplified, local feature flagging server that can be used with LanchDarkly SDKs. + This API allows for the syncing of flags with a remote LaunchDarkly project and to configure local overrides of + those flags. + version: 1.0.0 +servers: + - url: 'http' +paths: + /projects: + get: + summary: lists all projects that have been configured for the dev server + responses: //WIP + /projects/{projectKey}: + get: + summary: get the specified project and its configuration for syncing from the LaunchDarkly Service + parameters: + - $ref: '#/components/parameters/projectKey' + responses: // WIP + delete: + summary: remove the specified project from the dev server + parameters: + - $ref: '#/components/parameters/projectKey' + responses: //WIP + post: + summary: Add the project to the dev server + parameters: + - $ref: '#/components/parameters/projectKey' + responses: //WIP + /projects/{projectKey}/sourceEnvironment/{sourceEnvironmentKey}: + put: + summary: Change the source environment to the one specified. + parameters: + - $ref: '#/components/parameters/projectKey' + - name: sourceEnvironmentKey + required: true + in: path + schema: + type: string + responses: + 204: + description: OK + 404: + description: Not Found + content: + text/plain: + schema: + type: string + example: Environment not found + /projects/{projectKey}/overrides: + get: + summary: return all the flag overrides for the project + parameters: + - $ref: '#/components/parameters/projectKey' + responses: // WIP + delete: + summary: clear all overrides for the project + parameters: + - $ref: '#/components/parameters/projectKey' + responses: // WIP + /projects/{projectKey}/overrides/{flagKey}: + put: + summary: override flag value with value provided in the body + parameters: + - $ref: '#/components/parameters/projectKey' + - $ref: '#/components/parameters/flagKey' + requestBody: + required: true + description: flag value to override flag with. Should just be the json representation of the variation value. + content: + application/json: + schema: + oneOf: + - type: string + - type: boolean + - type: number + - type: object + responses: // WIP + delete: + summary: remove override for flag + parameters: + - $ref: '#/components/parameters/projectKey' + - $ref: '#/components/parameters/flagKey' + responses: // WIP + +components: + parameters: + flagKey: + name: flagKey + in: path + required: true + schema: + type: string + projectKey: + name: projectKey + in: path + required: true + schema: + type: string From 49013dbaed71cd6241097881bf3ee4ae550611dd Mon Sep 17 00:00:00 2001 From: Mike Zorn Date: Mon, 24 Jun 2024 10:05:09 -0700 Subject: [PATCH 02/79] Prefix everything with dev incase we also proxy apis with this thing --- internal/dev_server/overrides.yaml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/internal/dev_server/overrides.yaml b/internal/dev_server/overrides.yaml index 50bf3b1b..c81909e3 100644 --- a/internal/dev_server/overrides.yaml +++ b/internal/dev_server/overrides.yaml @@ -9,11 +9,11 @@ info: servers: - url: 'http' paths: - /projects: + /dev/projects: get: summary: lists all projects that have been configured for the dev server responses: //WIP - /projects/{projectKey}: + /dev/projects/{projectKey}: get: summary: get the specified project and its configuration for syncing from the LaunchDarkly Service parameters: @@ -29,7 +29,7 @@ paths: parameters: - $ref: '#/components/parameters/projectKey' responses: //WIP - /projects/{projectKey}/sourceEnvironment/{sourceEnvironmentKey}: + /dev/projects/{projectKey}/sourceEnvironment/{sourceEnvironmentKey}: put: summary: Change the source environment to the one specified. parameters: @@ -49,7 +49,7 @@ paths: schema: type: string example: Environment not found - /projects/{projectKey}/overrides: + /dev/projects/{projectKey}/overrides: get: summary: return all the flag overrides for the project parameters: @@ -60,7 +60,7 @@ paths: parameters: - $ref: '#/components/parameters/projectKey' responses: // WIP - /projects/{projectKey}/overrides/{flagKey}: + /dev/projects/{projectKey}/overrides/{flagKey}: put: summary: override flag value with value provided in the body parameters: From 0c2dc430a01731d27aa1888bd480d02a8149d94f Mon Sep 17 00:00:00 2001 From: Mike Zorn Date: Mon, 24 Jun 2024 10:44:59 -0700 Subject: [PATCH 03/79] Add project response --- internal/dev_server/overrides.yaml | 91 ++++++++++++++++++++++-------- 1 file changed, 68 insertions(+), 23 deletions(-) diff --git a/internal/dev_server/overrides.yaml b/internal/dev_server/overrides.yaml index c81909e3..b245270b 100644 --- a/internal/dev_server/overrides.yaml +++ b/internal/dev_server/overrides.yaml @@ -12,43 +12,64 @@ paths: /dev/projects: get: summary: lists all projects that have been configured for the dev server - responses: //WIP + responses: + 200: + description: OK. List of projects + content: + application/json: + schema: + description: list of project keys. + type: array + items: + type: string + uniqueItems: true /dev/projects/{projectKey}: get: summary: get the specified project and its configuration for syncing from the LaunchDarkly Service parameters: - $ref: '#/components/parameters/projectKey' - responses: // WIP + responses: + 200: + $ref: '#/components/responses/Project' + delete: summary: remove the specified project from the dev server parameters: - $ref: '#/components/parameters/projectKey' - responses: //WIP + responses: + 204: + description: OK. Project & overrides were removed post: summary: Add the project to the dev server parameters: - $ref: '#/components/parameters/projectKey' - responses: //WIP - /dev/projects/{projectKey}/sourceEnvironment/{sourceEnvironmentKey}: - put: - summary: Change the source environment to the one specified. - parameters: - - $ref: '#/components/parameters/projectKey' - - name: sourceEnvironmentKey - required: true - in: path - schema: - type: string - responses: - 204: - description: OK - 404: - description: Not Found - content: - text/plain: - schema: + requestBody: + content: + application/json: + schema: + type: object + required: + - sourceEnvironmentKey + properties: + sourceEnvironmentKey: type: string - example: Environment not found + description: environment to copy flag values from + context: + type: object + description: context object to use when evaluating flags in source environment + default: + key: "local-dev" + kind: user + responses: + 201: + $ref: '#/components/responses/Project' +# WIP updates +# patch: +# summary: change configuration of the project +# parameters: +# - $ref: '#/components/parameters/projectKey' +# responses: + /dev/projects/{projectKey}/overrides: get: summary: return all the flag overrides for the project @@ -99,3 +120,27 @@ components: required: true schema: type: string + responses: + Project: + description: Project representation + content: + application/json: + schema: + type: object + required: + - sourceEnvironmentKey + - context + - _lastSyncedFromSource + properties: + sourceEnvironmentKey: + type: string + description: environment to copy flag values from + context: + type: object + description: context object to use when evaluating flags in source environment + default: + key: "local-dev" + kind: user + _lastSyncedFromSource: + type: number + description: unix timestamp for the lat time the flag values were synced from the source environment From 1f199e680ecb33474c84497e4d8372ef557abf94 Mon Sep 17 00:00:00 2001 From: Mike Zorn Date: Mon, 24 Jun 2024 10:55:08 -0700 Subject: [PATCH 04/79] More complete spec --- internal/dev_server/overrides.yaml | 64 ++++++++++++++++++++---------- 1 file changed, 43 insertions(+), 21 deletions(-) diff --git a/internal/dev_server/overrides.yaml b/internal/dev_server/overrides.yaml index b245270b..50f9d2b3 100644 --- a/internal/dev_server/overrides.yaml +++ b/internal/dev_server/overrides.yaml @@ -63,24 +63,25 @@ paths: responses: 201: $ref: '#/components/responses/Project' -# WIP updates +# WIP updates can be added later # patch: # summary: change configuration of the project # parameters: # - $ref: '#/components/parameters/projectKey' # responses: - /dev/projects/{projectKey}/overrides: - get: - summary: return all the flag overrides for the project - parameters: - - $ref: '#/components/parameters/projectKey' - responses: // WIP - delete: - summary: clear all overrides for the project - parameters: - - $ref: '#/components/parameters/projectKey' - responses: // WIP +# WIP NOT SURE IF NEEDED TBH +# /dev/projects/{projectKey}/overrides: +# get: +# summary: return all the flag overrides for the project +# parameters: +# - $ref: '#/components/parameters/projectKey' +# responses: // WIP +# delete: +# summary: clear all overrides for the project +# parameters: +# - $ref: '#/components/parameters/projectKey' +# responses: // WIP /dev/projects/{projectKey}/overrides/{flagKey}: put: summary: override flag value with value provided in the body @@ -89,22 +90,23 @@ paths: - $ref: '#/components/parameters/flagKey' requestBody: required: true - description: flag value to override flag with. Should just be the json representation of the variation value. + description: flag value to override flag with. The json representation of the variation value. content: application/json: schema: - oneOf: - - type: string - - type: boolean - - type: number - - type: object - responses: // WIP + $ref: '#/components/schemas/FlagValue' + responses: + 200: + $ref: '#/components/responses/FlagOverride' + delete: summary: remove override for flag parameters: - $ref: '#/components/parameters/projectKey' - $ref: '#/components/parameters/flagKey' - responses: // WIP + responses: + 200: + $ref: '#/components/responses/FlagOverride' components: parameters: @@ -120,9 +122,29 @@ components: required: true schema: type: string + schemas: + FlagValue: + description: value of a feature flag variation + oneOf: + - type: string + - type: boolean + - type: number + - type: object responses: + FlagOverride: + description: Flag override + content: + application/json: + schema: + type: object + properties: + value: + $ref: '#/components/schemas/FlagValue' + override: + type: boolean + description: whether or not this is an overridden value or one from the source environment Project: - description: Project representation + description: Project content: application/json: schema: From 41d748f07d46a646aee81d6f02588a80d357d732 Mon Sep 17 00:00:00 2001 From: Mike Zorn Date: Mon, 24 Jun 2024 11:53:07 -0700 Subject: [PATCH 05/79] IT LIVES! Need to fix up some linter rules for tools & code gen --- go.mod | 21 +- go.sum | 49 +- internal/dev_server/api/Makefile | 2 + .../{overrides.yaml => api/api.yaml} | 5 +- internal/dev_server/api/oapi-codegen-cfg.yaml | 7 + internal/dev_server/api/server.gen.go | 854 + internal/dev_server/api/server.go | 39 + internal/dev_server/main.go | 24 + tools.go | 2 + .../apapsch/go-jsonmerge/v2/.editorconfig | 6 + .../apapsch/go-jsonmerge/v2/.gitattributes | 175 + .../apapsch/go-jsonmerge/v2/.gitignore | 11 + .../apapsch/go-jsonmerge/v2/.gitlab-ci.yml | 42 + .../apapsch/go-jsonmerge/v2/.travis.yml | 19 + .../apapsch/go-jsonmerge/v2/LICENSE | 21 + .../apapsch/go-jsonmerge/v2/README.md | 81 + .../apapsch/go-jsonmerge/v2/build.cmd | 25 + .../apapsch/go-jsonmerge/v2/build.sh | 19 + .../github.com/apapsch/go-jsonmerge/v2/doc.go | 52 + .../apapsch/go-jsonmerge/v2/merge.go | 167 + vendor/github.com/gorilla/mux/.editorconfig | 20 + vendor/github.com/gorilla/mux/.gitignore | 1 + vendor/github.com/gorilla/mux/LICENSE | 27 + vendor/github.com/gorilla/mux/Makefile | 34 + vendor/github.com/gorilla/mux/README.md | 812 + vendor/github.com/gorilla/mux/doc.go | 305 + vendor/github.com/gorilla/mux/middleware.go | 74 + vendor/github.com/gorilla/mux/mux.go | 608 + vendor/github.com/gorilla/mux/regexp.go | 388 + vendor/github.com/gorilla/mux/route.go | 765 + vendor/github.com/gorilla/mux/test_helpers.go | 19 + .../github.com/mattn/go-isatty/isatty_bsd.go | 3 +- .../mattn/go-isatty/isatty_others.go | 5 +- .../mattn/go-isatty/isatty_tcgets.go | 3 +- .../microcosm-cc/bluemonday/helpers.go | 6 +- .../microcosm-cc/bluemonday/policy.go | 51 + .../microcosm-cc/bluemonday/sanitize.go | 53 +- .../oapi-codegen/oapi-codegen/v2/LICENSE | 201 + .../v2/cmd/oapi-codegen/oapi-codegen.go | 549 + .../oapi-codegen/v2/pkg/codegen/codegen.go | 1185 ++ .../v2/pkg/codegen/configuration.go | 189 + .../oapi-codegen/v2/pkg/codegen/extension.go | 102 + .../v2/pkg/codegen/externalref.go | 80 + .../oapi-codegen/v2/pkg/codegen/filter.go | 88 + .../oapi-codegen/v2/pkg/codegen/inline.go | 77 + .../v2/pkg/codegen/merge_schemas.go | 249 + .../v2/pkg/codegen/merge_schemas_v1.go | 111 + .../oapi-codegen/v2/pkg/codegen/operations.go | 1098 + .../oapi-codegen/v2/pkg/codegen/prune.go | 489 + .../oapi-codegen/v2/pkg/codegen/schema.go | 879 + .../v2/pkg/codegen/template_helpers.go | 326 + .../templates/additional-properties.tmpl | 72 + .../codegen/templates/chi/chi-handler.tmpl | 50 + .../codegen/templates/chi/chi-interface.tmpl | 17 + .../codegen/templates/chi/chi-middleware.tmpl | 261 + .../templates/client-with-responses.tmpl | 120 + .../v2/pkg/codegen/templates/client.tmpl | 312 + .../v2/pkg/codegen/templates/constants.tmpl | 15 + .../templates/echo/echo-interface.tmpl | 7 + .../codegen/templates/echo/echo-register.tmpl | 33 + .../codegen/templates/echo/echo-wrappers.tmpl | 131 + .../templates/fiber/fiber-handler.tmpl | 25 + .../templates/fiber/fiber-interface.tmpl | 7 + .../templates/fiber/fiber-middleware.tmpl | 169 + .../codegen/templates/gin/gin-interface.tmpl | 7 + .../codegen/templates/gin/gin-register.tmpl | 33 + .../codegen/templates/gin/gin-wrappers.tmpl | 186 + .../templates/gorilla/gorilla-interface.tmpl | 7 + .../templates/gorilla/gorilla-middleware.tmpl | 261 + .../templates/gorilla/gorilla-register.tmpl | 49 + .../v2/pkg/codegen/templates/imports.tmpl | 50 + .../v2/pkg/codegen/templates/inline.tmpl | 87 + .../codegen/templates/iris/iris-handler.tmpl | 23 + .../templates/iris/iris-interface.tmpl | 7 + .../templates/iris/iris-middleware.tmpl | 161 + .../v2/pkg/codegen/templates/param-types.tmpl | 6 + .../pkg/codegen/templates/request-bodies.tmpl | 11 + .../templates/stdhttp/std-http-handler.tmpl | 49 + .../templates/stdhttp/std-http-interface.tmpl | 7 + .../stdhttp/std-http-middleware.tmpl | 261 + .../codegen/templates/strict/strict-echo.tmpl | 97 + .../strict/strict-fiber-interface.tmpl | 145 + .../templates/strict/strict-fiber.tmpl | 90 + .../codegen/templates/strict/strict-gin.tmpl | 106 + .../codegen/templates/strict/strict-http.tmpl | 121 + .../templates/strict/strict-interface.tmpl | 147 + .../strict/strict-iris-interface.tmpl | 147 + .../codegen/templates/strict/strict-iris.tmpl | 107 + .../templates/strict/strict-responses.tmpl | 41 + .../v2/pkg/codegen/templates/typedef.tmpl | 4 + .../union-and-additional-properties.tmpl | 72 + .../v2/pkg/codegen/templates/union.tmpl | 140 + .../v2/pkg/codegen/test_schema.json | 15 + .../v2/pkg/codegen/test_spec.yaml | 207 + .../oapi-codegen/v2/pkg/codegen/utils.go | 1066 + .../oapi-codegen/v2/pkg/util/inputmapping.go | 79 + .../oapi-codegen/v2/pkg/util/isjson.go | 14 + .../oapi-codegen/v2/pkg/util/loader.go | 37 + .../oapi-codegen/runtime/.gitignore | 1 + .../github.com/oapi-codegen/runtime/LICENSE | 201 + .../github.com/oapi-codegen/runtime/Makefile | 32 + .../github.com/oapi-codegen/runtime/README.md | 6 + .../github.com/oapi-codegen/runtime/bind.go | 24 + .../oapi-codegen/runtime/bindform.go | 318 + .../oapi-codegen/runtime/bindparam.go | 555 + .../oapi-codegen/runtime/bindstring.go | 174 + .../oapi-codegen/runtime/deepobject.go | 371 + .../oapi-codegen/runtime/jsonmerge.go | 34 + .../oapi-codegen/runtime/renovate.json | 6 + .../runtime/strictmiddleware/nethttp/main.go | 16 + .../oapi-codegen/runtime/styleparam.go | 478 + .../oapi-codegen/runtime/types/date.go | 43 + .../oapi-codegen/runtime/types/email.go | 40 + .../oapi-codegen/runtime/types/file.go | 71 + .../oapi-codegen/runtime/types/regexes.go | 11 + .../oapi-codegen/runtime/types/uuid.go | 7 + vendor/golang.org/x/mod/LICENSE | 27 + vendor/golang.org/x/mod/PATENTS | 22 + .../x/mod/internal/lazyregexp/lazyre.go | 78 + vendor/golang.org/x/mod/module/module.go | 841 + vendor/golang.org/x/mod/module/pseudo.go | 250 + vendor/golang.org/x/mod/semver/semver.go | 401 + vendor/golang.org/x/net/html/doc.go | 2 +- vendor/golang.org/x/sync/errgroup/errgroup.go | 3 + vendor/golang.org/x/sys/unix/asm_zos_s390x.s | 665 +- vendor/golang.org/x/sys/unix/bpxsvc_zos.go | 657 + vendor/golang.org/x/sys/unix/bpxsvc_zos.s | 192 + vendor/golang.org/x/sys/unix/epoll_zos.go | 220 - vendor/golang.org/x/sys/unix/fstatfs_zos.go | 163 - vendor/golang.org/x/sys/unix/mmap_nomremap.go | 2 +- vendor/golang.org/x/sys/unix/pagesize_unix.go | 2 +- .../x/sys/unix/readdirent_getdirentries.go | 2 +- vendor/golang.org/x/sys/unix/sockcmsg_zos.go | 58 + .../golang.org/x/sys/unix/symaddr_zos_s390x.s | 75 + .../x/sys/unix/syscall_zos_s390x.go | 1509 +- vendor/golang.org/x/sys/unix/sysvshm_unix.go | 2 +- .../x/sys/unix/sysvshm_unix_other.go | 2 +- vendor/golang.org/x/sys/unix/zerrors_linux.go | 9 + .../x/sys/unix/zerrors_zos_s390x.go | 233 +- .../x/sys/unix/zsymaddr_zos_s390x.s | 364 + .../x/sys/unix/zsyscall_zos_s390x.go | 3113 ++- .../x/sys/unix/zsysnum_linux_386.go | 5 + .../x/sys/unix/zsysnum_linux_amd64.go | 5 + .../x/sys/unix/zsysnum_linux_arm.go | 5 + .../x/sys/unix/zsysnum_linux_arm64.go | 5 + .../x/sys/unix/zsysnum_linux_loong64.go | 5 + .../x/sys/unix/zsysnum_linux_mips.go | 5 + .../x/sys/unix/zsysnum_linux_mips64.go | 5 + .../x/sys/unix/zsysnum_linux_mips64le.go | 5 + .../x/sys/unix/zsysnum_linux_mipsle.go | 5 + .../x/sys/unix/zsysnum_linux_ppc.go | 5 + .../x/sys/unix/zsysnum_linux_ppc64.go | 5 + .../x/sys/unix/zsysnum_linux_ppc64le.go | 5 + .../x/sys/unix/zsysnum_linux_riscv64.go | 5 + .../x/sys/unix/zsysnum_linux_s390x.go | 5 + .../x/sys/unix/zsysnum_linux_sparc64.go | 5 + .../x/sys/unix/zsysnum_zos_s390x.go | 5507 ++--- vendor/golang.org/x/sys/unix/ztypes_linux.go | 26 +- .../golang.org/x/sys/unix/ztypes_linux_386.go | 8 - .../x/sys/unix/ztypes_linux_amd64.go | 9 - .../golang.org/x/sys/unix/ztypes_linux_arm.go | 9 - .../x/sys/unix/ztypes_linux_arm64.go | 9 - .../x/sys/unix/ztypes_linux_loong64.go | 9 - .../x/sys/unix/ztypes_linux_mips.go | 9 - .../x/sys/unix/ztypes_linux_mips64.go | 9 - .../x/sys/unix/ztypes_linux_mips64le.go | 9 - .../x/sys/unix/ztypes_linux_mipsle.go | 9 - .../golang.org/x/sys/unix/ztypes_linux_ppc.go | 9 - .../x/sys/unix/ztypes_linux_ppc64.go | 9 - .../x/sys/unix/ztypes_linux_ppc64le.go | 9 - .../x/sys/unix/ztypes_linux_riscv64.go | 9 - .../x/sys/unix/ztypes_linux_s390x.go | 9 - .../x/sys/unix/ztypes_linux_sparc64.go | 9 - .../golang.org/x/sys/unix/ztypes_zos_s390x.go | 146 +- vendor/golang.org/x/sys/windows/aliases.go | 2 +- .../x/sys/windows/syscall_windows.go | 82 + .../golang.org/x/sys/windows/types_windows.go | 24 + .../x/sys/windows/zsyscall_windows.go | 126 +- vendor/golang.org/x/text/cases/cases.go | 162 + vendor/golang.org/x/text/cases/context.go | 376 + vendor/golang.org/x/text/cases/fold.go | 34 + vendor/golang.org/x/text/cases/icu.go | 61 + vendor/golang.org/x/text/cases/info.go | 82 + vendor/golang.org/x/text/cases/map.go | 816 + .../golang.org/x/text/cases/tables10.0.0.go | 2255 ++ .../golang.org/x/text/cases/tables11.0.0.go | 2316 +++ .../golang.org/x/text/cases/tables12.0.0.go | 2359 +++ .../golang.org/x/text/cases/tables13.0.0.go | 2399 +++ .../golang.org/x/text/cases/tables15.0.0.go | 2527 +++ vendor/golang.org/x/text/cases/tables9.0.0.go | 2215 ++ vendor/golang.org/x/text/cases/trieval.go | 217 + vendor/golang.org/x/text/internal/internal.go | 49 + .../x/text/internal/language/common.go | 16 + .../x/text/internal/language/compact.go | 29 + .../text/internal/language/compact/compact.go | 61 + .../internal/language/compact/language.go | 260 + .../text/internal/language/compact/parents.go | 120 + .../text/internal/language/compact/tables.go | 1015 + .../x/text/internal/language/compact/tags.go | 91 + .../x/text/internal/language/compose.go | 167 + .../x/text/internal/language/coverage.go | 28 + .../x/text/internal/language/language.go | 627 + .../x/text/internal/language/lookup.go | 412 + .../x/text/internal/language/match.go | 226 + .../x/text/internal/language/parse.go | 608 + .../x/text/internal/language/tables.go | 3494 ++++ .../x/text/internal/language/tags.go | 48 + vendor/golang.org/x/text/internal/match.go | 67 + vendor/golang.org/x/text/internal/tag/tag.go | 100 + vendor/golang.org/x/text/language/coverage.go | 187 + vendor/golang.org/x/text/language/doc.go | 98 + vendor/golang.org/x/text/language/language.go | 605 + vendor/golang.org/x/text/language/match.go | 735 + vendor/golang.org/x/text/language/parse.go | 256 + vendor/golang.org/x/text/language/tables.go | 298 + vendor/golang.org/x/text/language/tags.go | 145 + vendor/golang.org/x/tools/LICENSE | 27 + vendor/golang.org/x/tools/PATENTS | 22 + .../x/tools/go/ast/astutil/enclosing.go | 634 + .../x/tools/go/ast/astutil/imports.go | 485 + .../x/tools/go/ast/astutil/rewrite.go | 486 + .../golang.org/x/tools/go/ast/astutil/util.go | 18 + vendor/golang.org/x/tools/imports/forward.go | 77 + .../x/tools/internal/event/core/event.go | 85 + .../x/tools/internal/event/core/export.go | 70 + .../x/tools/internal/event/core/fast.go | 77 + .../empty.s => tools/internal/event/doc.go} | 7 +- .../x/tools/internal/event/event.go | 127 + .../x/tools/internal/event/keys/keys.go | 564 + .../x/tools/internal/event/keys/standard.go | 22 + .../x/tools/internal/event/keys/util.go | 21 + .../x/tools/internal/event/label/label.go | 215 + .../x/tools/internal/gocommand/invoke.go | 470 + .../x/tools/internal/gocommand/vendor.go | 163 + .../x/tools/internal/gocommand/version.go | 71 + .../x/tools/internal/gopathwalk/walk.go | 337 + .../x/tools/internal/imports/fix.go | 1892 ++ .../x/tools/internal/imports/imports.go | 354 + .../x/tools/internal/imports/mod.go | 841 + .../x/tools/internal/imports/mod_cache.go | 331 + .../x/tools/internal/imports/sortimports.go | 297 + .../x/tools/internal/stdlib/manifest.go | 17320 ++++++++++++++++ .../x/tools/internal/stdlib/stdlib.go | 97 + vendor/gopkg.in/yaml.v2/.travis.yml | 17 + vendor/gopkg.in/yaml.v2/LICENSE | 201 + vendor/gopkg.in/yaml.v2/LICENSE.libyaml | 31 + vendor/gopkg.in/yaml.v2/NOTICE | 13 + vendor/gopkg.in/yaml.v2/README.md | 133 + vendor/gopkg.in/yaml.v2/apic.go | 744 + vendor/gopkg.in/yaml.v2/decode.go | 815 + vendor/gopkg.in/yaml.v2/emitterc.go | 1685 ++ vendor/gopkg.in/yaml.v2/encode.go | 390 + vendor/gopkg.in/yaml.v2/parserc.go | 1095 + vendor/gopkg.in/yaml.v2/readerc.go | 412 + vendor/gopkg.in/yaml.v2/resolve.go | 258 + vendor/gopkg.in/yaml.v2/scannerc.go | 2711 +++ vendor/gopkg.in/yaml.v2/sorter.go | 113 + vendor/gopkg.in/yaml.v2/writerc.go | 26 + vendor/gopkg.in/yaml.v2/yaml.go | 478 + vendor/gopkg.in/yaml.v2/yamlh.go | 739 + vendor/gopkg.in/yaml.v2/yamlprivateh.go | 173 + vendor/modules.txt | 56 +- 262 files changed, 89306 insertions(+), 4334 deletions(-) create mode 100644 internal/dev_server/api/Makefile rename internal/dev_server/{overrides.yaml => api/api.yaml} (98%) create mode 100644 internal/dev_server/api/oapi-codegen-cfg.yaml create mode 100644 internal/dev_server/api/server.gen.go create mode 100644 internal/dev_server/api/server.go create mode 100644 internal/dev_server/main.go create mode 100644 vendor/github.com/apapsch/go-jsonmerge/v2/.editorconfig create mode 100644 vendor/github.com/apapsch/go-jsonmerge/v2/.gitattributes create mode 100644 vendor/github.com/apapsch/go-jsonmerge/v2/.gitignore create mode 100644 vendor/github.com/apapsch/go-jsonmerge/v2/.gitlab-ci.yml create mode 100644 vendor/github.com/apapsch/go-jsonmerge/v2/.travis.yml create mode 100644 vendor/github.com/apapsch/go-jsonmerge/v2/LICENSE create mode 100644 vendor/github.com/apapsch/go-jsonmerge/v2/README.md create mode 100644 vendor/github.com/apapsch/go-jsonmerge/v2/build.cmd create mode 100644 vendor/github.com/apapsch/go-jsonmerge/v2/build.sh create mode 100644 vendor/github.com/apapsch/go-jsonmerge/v2/doc.go create mode 100644 vendor/github.com/apapsch/go-jsonmerge/v2/merge.go create mode 100644 vendor/github.com/gorilla/mux/.editorconfig create mode 100644 vendor/github.com/gorilla/mux/.gitignore create mode 100644 vendor/github.com/gorilla/mux/LICENSE create mode 100644 vendor/github.com/gorilla/mux/Makefile create mode 100644 vendor/github.com/gorilla/mux/README.md create mode 100644 vendor/github.com/gorilla/mux/doc.go create mode 100644 vendor/github.com/gorilla/mux/middleware.go create mode 100644 vendor/github.com/gorilla/mux/mux.go create mode 100644 vendor/github.com/gorilla/mux/regexp.go create mode 100644 vendor/github.com/gorilla/mux/route.go create mode 100644 vendor/github.com/gorilla/mux/test_helpers.go create mode 100644 vendor/github.com/oapi-codegen/oapi-codegen/v2/LICENSE create mode 100644 vendor/github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen/oapi-codegen.go create mode 100644 vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/codegen.go create mode 100644 vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/configuration.go create mode 100644 vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/extension.go create mode 100644 vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/externalref.go create mode 100644 vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/filter.go create mode 100644 vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/inline.go create mode 100644 vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/merge_schemas.go create mode 100644 vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/merge_schemas_v1.go create mode 100644 vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/operations.go create mode 100644 vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/prune.go create mode 100644 vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/schema.go create mode 100644 vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/template_helpers.go create mode 100644 vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/templates/additional-properties.tmpl create mode 100644 vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/templates/chi/chi-handler.tmpl create mode 100644 vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/templates/chi/chi-interface.tmpl create mode 100644 vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/templates/chi/chi-middleware.tmpl create mode 100644 vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/templates/client-with-responses.tmpl create mode 100644 vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/templates/client.tmpl create mode 100644 vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/templates/constants.tmpl create mode 100644 vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/templates/echo/echo-interface.tmpl create mode 100644 vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/templates/echo/echo-register.tmpl create mode 100644 vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/templates/echo/echo-wrappers.tmpl create mode 100644 vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/templates/fiber/fiber-handler.tmpl create mode 100644 vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/templates/fiber/fiber-interface.tmpl create mode 100644 vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/templates/fiber/fiber-middleware.tmpl create mode 100644 vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/templates/gin/gin-interface.tmpl create mode 100644 vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/templates/gin/gin-register.tmpl create mode 100644 vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/templates/gin/gin-wrappers.tmpl create mode 100644 vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/templates/gorilla/gorilla-interface.tmpl create mode 100644 vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/templates/gorilla/gorilla-middleware.tmpl create mode 100644 vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/templates/gorilla/gorilla-register.tmpl create mode 100644 vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/templates/imports.tmpl create mode 100644 vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/templates/inline.tmpl create mode 100644 vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/templates/iris/iris-handler.tmpl create mode 100644 vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/templates/iris/iris-interface.tmpl create mode 100644 vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/templates/iris/iris-middleware.tmpl create mode 100644 vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/templates/param-types.tmpl create mode 100644 vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/templates/request-bodies.tmpl create mode 100644 vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/templates/stdhttp/std-http-handler.tmpl create mode 100644 vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/templates/stdhttp/std-http-interface.tmpl create mode 100644 vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/templates/stdhttp/std-http-middleware.tmpl create mode 100644 vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/templates/strict/strict-echo.tmpl create mode 100644 vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/templates/strict/strict-fiber-interface.tmpl create mode 100644 vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/templates/strict/strict-fiber.tmpl create mode 100644 vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/templates/strict/strict-gin.tmpl create mode 100644 vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/templates/strict/strict-http.tmpl create mode 100644 vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/templates/strict/strict-interface.tmpl create mode 100644 vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/templates/strict/strict-iris-interface.tmpl create mode 100644 vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/templates/strict/strict-iris.tmpl create mode 100644 vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/templates/strict/strict-responses.tmpl create mode 100644 vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/templates/typedef.tmpl create mode 100644 vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/templates/union-and-additional-properties.tmpl create mode 100644 vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/templates/union.tmpl create mode 100644 vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/test_schema.json create mode 100644 vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/test_spec.yaml create mode 100644 vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen/utils.go create mode 100644 vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/util/inputmapping.go create mode 100644 vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/util/isjson.go create mode 100644 vendor/github.com/oapi-codegen/oapi-codegen/v2/pkg/util/loader.go create mode 100644 vendor/github.com/oapi-codegen/runtime/.gitignore create mode 100644 vendor/github.com/oapi-codegen/runtime/LICENSE create mode 100644 vendor/github.com/oapi-codegen/runtime/Makefile create mode 100644 vendor/github.com/oapi-codegen/runtime/README.md create mode 100644 vendor/github.com/oapi-codegen/runtime/bind.go create mode 100644 vendor/github.com/oapi-codegen/runtime/bindform.go create mode 100644 vendor/github.com/oapi-codegen/runtime/bindparam.go create mode 100644 vendor/github.com/oapi-codegen/runtime/bindstring.go create mode 100644 vendor/github.com/oapi-codegen/runtime/deepobject.go create mode 100644 vendor/github.com/oapi-codegen/runtime/jsonmerge.go create mode 100644 vendor/github.com/oapi-codegen/runtime/renovate.json create mode 100644 vendor/github.com/oapi-codegen/runtime/strictmiddleware/nethttp/main.go create mode 100644 vendor/github.com/oapi-codegen/runtime/styleparam.go create mode 100644 vendor/github.com/oapi-codegen/runtime/types/date.go create mode 100644 vendor/github.com/oapi-codegen/runtime/types/email.go create mode 100644 vendor/github.com/oapi-codegen/runtime/types/file.go create mode 100644 vendor/github.com/oapi-codegen/runtime/types/regexes.go create mode 100644 vendor/github.com/oapi-codegen/runtime/types/uuid.go create mode 100644 vendor/golang.org/x/mod/LICENSE create mode 100644 vendor/golang.org/x/mod/PATENTS create mode 100644 vendor/golang.org/x/mod/internal/lazyregexp/lazyre.go create mode 100644 vendor/golang.org/x/mod/module/module.go create mode 100644 vendor/golang.org/x/mod/module/pseudo.go create mode 100644 vendor/golang.org/x/mod/semver/semver.go create mode 100644 vendor/golang.org/x/sys/unix/bpxsvc_zos.go create mode 100644 vendor/golang.org/x/sys/unix/bpxsvc_zos.s delete mode 100644 vendor/golang.org/x/sys/unix/epoll_zos.go delete mode 100644 vendor/golang.org/x/sys/unix/fstatfs_zos.go create mode 100644 vendor/golang.org/x/sys/unix/sockcmsg_zos.go create mode 100644 vendor/golang.org/x/sys/unix/symaddr_zos_s390x.s create mode 100644 vendor/golang.org/x/sys/unix/zsymaddr_zos_s390x.s create mode 100644 vendor/golang.org/x/text/cases/cases.go create mode 100644 vendor/golang.org/x/text/cases/context.go create mode 100644 vendor/golang.org/x/text/cases/fold.go create mode 100644 vendor/golang.org/x/text/cases/icu.go create mode 100644 vendor/golang.org/x/text/cases/info.go create mode 100644 vendor/golang.org/x/text/cases/map.go create mode 100644 vendor/golang.org/x/text/cases/tables10.0.0.go create mode 100644 vendor/golang.org/x/text/cases/tables11.0.0.go create mode 100644 vendor/golang.org/x/text/cases/tables12.0.0.go create mode 100644 vendor/golang.org/x/text/cases/tables13.0.0.go create mode 100644 vendor/golang.org/x/text/cases/tables15.0.0.go create mode 100644 vendor/golang.org/x/text/cases/tables9.0.0.go create mode 100644 vendor/golang.org/x/text/cases/trieval.go create mode 100644 vendor/golang.org/x/text/internal/internal.go create mode 100644 vendor/golang.org/x/text/internal/language/common.go create mode 100644 vendor/golang.org/x/text/internal/language/compact.go create mode 100644 vendor/golang.org/x/text/internal/language/compact/compact.go create mode 100644 vendor/golang.org/x/text/internal/language/compact/language.go create mode 100644 vendor/golang.org/x/text/internal/language/compact/parents.go create mode 100644 vendor/golang.org/x/text/internal/language/compact/tables.go create mode 100644 vendor/golang.org/x/text/internal/language/compact/tags.go create mode 100644 vendor/golang.org/x/text/internal/language/compose.go create mode 100644 vendor/golang.org/x/text/internal/language/coverage.go create mode 100644 vendor/golang.org/x/text/internal/language/language.go create mode 100644 vendor/golang.org/x/text/internal/language/lookup.go create mode 100644 vendor/golang.org/x/text/internal/language/match.go create mode 100644 vendor/golang.org/x/text/internal/language/parse.go create mode 100644 vendor/golang.org/x/text/internal/language/tables.go create mode 100644 vendor/golang.org/x/text/internal/language/tags.go create mode 100644 vendor/golang.org/x/text/internal/match.go create mode 100644 vendor/golang.org/x/text/internal/tag/tag.go create mode 100644 vendor/golang.org/x/text/language/coverage.go create mode 100644 vendor/golang.org/x/text/language/doc.go create mode 100644 vendor/golang.org/x/text/language/language.go create mode 100644 vendor/golang.org/x/text/language/match.go create mode 100644 vendor/golang.org/x/text/language/parse.go create mode 100644 vendor/golang.org/x/text/language/tables.go create mode 100644 vendor/golang.org/x/text/language/tags.go create mode 100644 vendor/golang.org/x/tools/LICENSE create mode 100644 vendor/golang.org/x/tools/PATENTS create mode 100644 vendor/golang.org/x/tools/go/ast/astutil/enclosing.go create mode 100644 vendor/golang.org/x/tools/go/ast/astutil/imports.go create mode 100644 vendor/golang.org/x/tools/go/ast/astutil/rewrite.go create mode 100644 vendor/golang.org/x/tools/go/ast/astutil/util.go create mode 100644 vendor/golang.org/x/tools/imports/forward.go create mode 100644 vendor/golang.org/x/tools/internal/event/core/event.go create mode 100644 vendor/golang.org/x/tools/internal/event/core/export.go create mode 100644 vendor/golang.org/x/tools/internal/event/core/fast.go rename vendor/golang.org/x/{sys/windows/empty.s => tools/internal/event/doc.go} (51%) create mode 100644 vendor/golang.org/x/tools/internal/event/event.go create mode 100644 vendor/golang.org/x/tools/internal/event/keys/keys.go create mode 100644 vendor/golang.org/x/tools/internal/event/keys/standard.go create mode 100644 vendor/golang.org/x/tools/internal/event/keys/util.go create mode 100644 vendor/golang.org/x/tools/internal/event/label/label.go create mode 100644 vendor/golang.org/x/tools/internal/gocommand/invoke.go create mode 100644 vendor/golang.org/x/tools/internal/gocommand/vendor.go create mode 100644 vendor/golang.org/x/tools/internal/gocommand/version.go create mode 100644 vendor/golang.org/x/tools/internal/gopathwalk/walk.go create mode 100644 vendor/golang.org/x/tools/internal/imports/fix.go create mode 100644 vendor/golang.org/x/tools/internal/imports/imports.go create mode 100644 vendor/golang.org/x/tools/internal/imports/mod.go create mode 100644 vendor/golang.org/x/tools/internal/imports/mod_cache.go create mode 100644 vendor/golang.org/x/tools/internal/imports/sortimports.go create mode 100644 vendor/golang.org/x/tools/internal/stdlib/manifest.go create mode 100644 vendor/golang.org/x/tools/internal/stdlib/stdlib.go create mode 100644 vendor/gopkg.in/yaml.v2/.travis.yml create mode 100644 vendor/gopkg.in/yaml.v2/LICENSE create mode 100644 vendor/gopkg.in/yaml.v2/LICENSE.libyaml create mode 100644 vendor/gopkg.in/yaml.v2/NOTICE create mode 100644 vendor/gopkg.in/yaml.v2/README.md create mode 100644 vendor/gopkg.in/yaml.v2/apic.go create mode 100644 vendor/gopkg.in/yaml.v2/decode.go create mode 100644 vendor/gopkg.in/yaml.v2/emitterc.go create mode 100644 vendor/gopkg.in/yaml.v2/encode.go create mode 100644 vendor/gopkg.in/yaml.v2/parserc.go create mode 100644 vendor/gopkg.in/yaml.v2/readerc.go create mode 100644 vendor/gopkg.in/yaml.v2/resolve.go create mode 100644 vendor/gopkg.in/yaml.v2/scannerc.go create mode 100644 vendor/gopkg.in/yaml.v2/sorter.go create mode 100644 vendor/gopkg.in/yaml.v2/writerc.go create mode 100644 vendor/gopkg.in/yaml.v2/yaml.go create mode 100644 vendor/gopkg.in/yaml.v2/yamlh.go create mode 100644 vendor/gopkg.in/yaml.v2/yamlprivateh.go diff --git a/go.mod b/go.mod index d874a332..aec32dfb 100644 --- a/go.mod +++ b/go.mod @@ -9,21 +9,25 @@ require ( github.com/charmbracelet/lipgloss v0.10.0 github.com/getkin/kin-openapi v0.124.0 github.com/google/uuid v1.6.0 + github.com/gorilla/mux v1.8.1 github.com/iancoleman/strcase v0.3.0 github.com/launchdarkly/api-client-go/v14 v14.0.0 github.com/mitchellh/go-homedir v1.1.0 github.com/muesli/reflow v0.3.0 + github.com/oapi-codegen/oapi-codegen/v2 v2.3.0 + github.com/oapi-codegen/runtime v1.1.1 github.com/pkg/errors v0.9.1 github.com/spf13/cobra v1.8.0 github.com/spf13/pflag v1.0.5 github.com/spf13/viper v1.18.2 github.com/stretchr/testify v1.9.0 - golang.org/x/term v0.18.0 + golang.org/x/term v0.20.0 gopkg.in/yaml.v3 v3.0.1 ) require ( github.com/alecthomas/chroma v0.10.0 // indirect + github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect github.com/atotto/clipboard v0.1.4 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/aymerick/douceur v0.2.0 // indirect @@ -42,10 +46,10 @@ require ( github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/mailru/easyjson v0.7.7 // indirect - github.com/mattn/go-isatty v0.0.18 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-localereader v0.0.1 // indirect github.com/mattn/go-runewidth v0.0.15 // indirect - github.com/microcosm-cc/bluemonday v1.0.21 // indirect + github.com/microcosm-cc/bluemonday v1.0.25 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b // indirect @@ -69,12 +73,15 @@ require ( go.uber.org/atomic v1.9.0 // indirect go.uber.org/multierr v1.9.0 // indirect golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect - golang.org/x/net v0.23.0 // indirect + golang.org/x/mod v0.17.0 // indirect + golang.org/x/net v0.25.0 // indirect golang.org/x/oauth2 v0.15.0 // indirect - golang.org/x/sync v0.5.0 // indirect - golang.org/x/sys v0.18.0 // indirect - golang.org/x/text v0.14.0 // indirect + golang.org/x/sync v0.7.0 // indirect + golang.org/x/sys v0.20.0 // indirect + golang.org/x/text v0.15.0 // indirect + golang.org/x/tools v0.21.0 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/protobuf v1.33.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect ) diff --git a/go.sum b/go.sum index 1b1ea4f7..725043c4 100644 --- a/go.sum +++ b/go.sum @@ -33,8 +33,11 @@ cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9 dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk= github.com/alecthomas/chroma v0.10.0 h1:7XDcGkCQopCNKjZHfYrNLraA+M7e0fMiJ/Mfikbfjek= github.com/alecthomas/chroma v0.10.0/go.mod h1:jtJATyUxlIORhUOFNA9NZDWGAQ8wpxQQqNSB4rjA/1s= +github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ= +github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk= github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/aymanbagabas/go-osc52 v1.0.3/go.mod h1:zT8H+Rk4VSabYN90pWyugflM3ZhpTZNC7cASDfUCdT4= @@ -42,6 +45,7 @@ github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiE github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= +github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/charmbracelet/bubbles v0.18.0 h1:PYv1A036luoBGroX6VWjQIE9Syf2Wby2oOl/39KLfy0= github.com/charmbracelet/bubbles v0.18.0/go.mod h1:08qhZhtIwzgrtBjAcJnij1t1H0ZRjwHyGsy6AL11PSw= @@ -119,7 +123,7 @@ github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= @@ -136,6 +140,8 @@ github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+ github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY= github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c= +github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= @@ -151,6 +157,7 @@ github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8Hm github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= @@ -167,8 +174,8 @@ github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3v github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98= -github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= @@ -176,8 +183,9 @@ github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRC github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= -github.com/microcosm-cc/bluemonday v1.0.21 h1:dNH3e4PSyE4vNX+KlRGHT5KrSvjeUkoNPwEORjffHJg= github.com/microcosm-cc/bluemonday v1.0.21/go.mod h1:ytNkv4RrDrLJ2pqlsSI46O6IVXmZOBBD4SaJyDwwTkM= +github.com/microcosm-cc/bluemonday v1.0.25 h1:4NEwSfiJ+Wva0VxN5B8OwMicaJvD8r9tlJWm9rtloEg= +github.com/microcosm-cc/bluemonday v1.0.25/go.mod h1:ZIOjCQp1OrzBBPIJmfX4qDYFuhU02nx4bn030ixfHLE= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= @@ -193,6 +201,10 @@ github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKt github.com/muesli/termenv v0.13.0/go.mod h1:sP1+uffeLaEYpyOTb8pLCUctGcGLnoFjSn4YJK5e2bc= github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= +github.com/oapi-codegen/oapi-codegen/v2 v2.3.0 h1:rICjNsHbPP1LttefanBPnwsSwl09SqhCO7Ee623qR84= +github.com/oapi-codegen/oapi-codegen/v2 v2.3.0/go.mod h1:4k+cJeSq5ntkwlcpQSxLxICCxQzCL772o30PxdibRt4= +github.com/oapi-codegen/runtime v1.1.1 h1:EXLHh0DXIJnWhdRPN2w4MXAzFyE4CskzhNLUmtpMYro= +github.com/oapi-codegen/runtime v1.1.1/go.mod h1:SK9X900oXmPWilYR5/WKPzt3Kqxn/uS/+lbpREv+eCg= github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4= @@ -230,6 +242,7 @@ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ= github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk= +github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -245,7 +258,7 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= -github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0= +github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -300,6 +313,8 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -327,8 +342,8 @@ golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81R golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20221002022538-bcab6841153b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= -golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= -golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -345,8 +360,8 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= -golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -377,19 +392,19 @@ golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= -golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8= -golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= +golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw= +golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -433,6 +448,8 @@ golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roY golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.21.0 h1:qc0xYgIbsSDt9EyWz05J5wfa7LOVW0YTLOXrqdLAWIw= +golang.org/x/tools v0.21.0/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -523,6 +540,8 @@ gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/internal/dev_server/api/Makefile b/internal/dev_server/api/Makefile new file mode 100644 index 00000000..8989ff27 --- /dev/null +++ b/internal/dev_server/api/Makefile @@ -0,0 +1,2 @@ +server.gen.go: api.yaml oapi-codegen-cfg.yaml + oapi-codegen -config oapi-codegen-cfg.yaml api.yaml \ No newline at end of file diff --git a/internal/dev_server/overrides.yaml b/internal/dev_server/api/api.yaml similarity index 98% rename from internal/dev_server/overrides.yaml rename to internal/dev_server/api/api.yaml index 50f9d2b3..11005582 100644 --- a/internal/dev_server/overrides.yaml +++ b/internal/dev_server/api/api.yaml @@ -137,6 +137,9 @@ components: application/json: schema: type: object + required: + - override + - value properties: value: $ref: '#/components/schemas/FlagValue' @@ -164,5 +167,5 @@ components: key: "local-dev" kind: user _lastSyncedFromSource: - type: number + type: integer description: unix timestamp for the lat time the flag values were synced from the source environment diff --git a/internal/dev_server/api/oapi-codegen-cfg.yaml b/internal/dev_server/api/oapi-codegen-cfg.yaml new file mode 100644 index 00000000..c1cba5fe --- /dev/null +++ b/internal/dev_server/api/oapi-codegen-cfg.yaml @@ -0,0 +1,7 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/deepmap/oapi-codegen/HEAD/configuration-schema.json +package: api +generate: + gorilla-server: true + models: true + strict-server: true +output: server.gen.go \ No newline at end of file diff --git a/internal/dev_server/api/server.gen.go b/internal/dev_server/api/server.gen.go new file mode 100644 index 00000000..7b9c43ae --- /dev/null +++ b/internal/dev_server/api/server.gen.go @@ -0,0 +1,854 @@ +// Package api provides primitives to interact with the openapi HTTP API. +// +// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.3.0 DO NOT EDIT. +package api + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + + "github.com/gorilla/mux" + "github.com/oapi-codegen/runtime" + strictnethttp "github.com/oapi-codegen/runtime/strictmiddleware/nethttp" +) + +// FlagValue value of a feature flag variation +type FlagValue struct { + union json.RawMessage +} + +// FlagValue0 defines model for . +type FlagValue0 = string + +// FlagValue1 defines model for . +type FlagValue1 = bool + +// FlagValue2 defines model for . +type FlagValue2 = float32 + +// FlagValue3 defines model for . +type FlagValue3 = map[string]interface{} + +// FlagKey defines model for flagKey. +type FlagKey = string + +// ProjectKey defines model for projectKey. +type ProjectKey = string + +// FlagOverride defines model for FlagOverride. +type FlagOverride struct { + // Override whether or not this is an overridden value or one from the source environment + Override bool `json:"override"` + + // Value value of a feature flag variation + Value FlagValue `json:"value"` +} + +// Project defines model for Project. +type Project struct { + // LastSyncedFromSource unix timestamp for the lat time the flag values were synced from the source environment + LastSyncedFromSource int `json:"_lastSyncedFromSource"` + + // Context context object to use when evaluating flags in source environment + Context map[string]interface{} `json:"context"` + + // SourceEnvironmentKey environment to copy flag values from + SourceEnvironmentKey string `json:"sourceEnvironmentKey"` +} + +// PostDevProjectsProjectKeyJSONBody defines parameters for PostDevProjectsProjectKey. +type PostDevProjectsProjectKeyJSONBody struct { + // Context context object to use when evaluating flags in source environment + Context *map[string]interface{} `json:"context,omitempty"` + + // SourceEnvironmentKey environment to copy flag values from + SourceEnvironmentKey string `json:"sourceEnvironmentKey"` +} + +// PostDevProjectsProjectKeyJSONRequestBody defines body for PostDevProjectsProjectKey for application/json ContentType. +type PostDevProjectsProjectKeyJSONRequestBody PostDevProjectsProjectKeyJSONBody + +// PutDevProjectsProjectKeyOverridesFlagKeyJSONRequestBody defines body for PutDevProjectsProjectKeyOverridesFlagKey for application/json ContentType. +type PutDevProjectsProjectKeyOverridesFlagKeyJSONRequestBody = FlagValue + +// AsFlagValue0 returns the union data inside the FlagValue as a FlagValue0 +func (t FlagValue) AsFlagValue0() (FlagValue0, error) { + var body FlagValue0 + err := json.Unmarshal(t.union, &body) + return body, err +} + +// FromFlagValue0 overwrites any union data inside the FlagValue as the provided FlagValue0 +func (t *FlagValue) FromFlagValue0(v FlagValue0) error { + b, err := json.Marshal(v) + t.union = b + return err +} + +// MergeFlagValue0 performs a merge with any union data inside the FlagValue, using the provided FlagValue0 +func (t *FlagValue) MergeFlagValue0(v FlagValue0) error { + b, err := json.Marshal(v) + if err != nil { + return err + } + + merged, err := runtime.JSONMerge(t.union, b) + t.union = merged + return err +} + +// AsFlagValue1 returns the union data inside the FlagValue as a FlagValue1 +func (t FlagValue) AsFlagValue1() (FlagValue1, error) { + var body FlagValue1 + err := json.Unmarshal(t.union, &body) + return body, err +} + +// FromFlagValue1 overwrites any union data inside the FlagValue as the provided FlagValue1 +func (t *FlagValue) FromFlagValue1(v FlagValue1) error { + b, err := json.Marshal(v) + t.union = b + return err +} + +// MergeFlagValue1 performs a merge with any union data inside the FlagValue, using the provided FlagValue1 +func (t *FlagValue) MergeFlagValue1(v FlagValue1) error { + b, err := json.Marshal(v) + if err != nil { + return err + } + + merged, err := runtime.JSONMerge(t.union, b) + t.union = merged + return err +} + +// AsFlagValue2 returns the union data inside the FlagValue as a FlagValue2 +func (t FlagValue) AsFlagValue2() (FlagValue2, error) { + var body FlagValue2 + err := json.Unmarshal(t.union, &body) + return body, err +} + +// FromFlagValue2 overwrites any union data inside the FlagValue as the provided FlagValue2 +func (t *FlagValue) FromFlagValue2(v FlagValue2) error { + b, err := json.Marshal(v) + t.union = b + return err +} + +// MergeFlagValue2 performs a merge with any union data inside the FlagValue, using the provided FlagValue2 +func (t *FlagValue) MergeFlagValue2(v FlagValue2) error { + b, err := json.Marshal(v) + if err != nil { + return err + } + + merged, err := runtime.JSONMerge(t.union, b) + t.union = merged + return err +} + +// AsFlagValue3 returns the union data inside the FlagValue as a FlagValue3 +func (t FlagValue) AsFlagValue3() (FlagValue3, error) { + var body FlagValue3 + err := json.Unmarshal(t.union, &body) + return body, err +} + +// FromFlagValue3 overwrites any union data inside the FlagValue as the provided FlagValue3 +func (t *FlagValue) FromFlagValue3(v FlagValue3) error { + b, err := json.Marshal(v) + t.union = b + return err +} + +// MergeFlagValue3 performs a merge with any union data inside the FlagValue, using the provided FlagValue3 +func (t *FlagValue) MergeFlagValue3(v FlagValue3) error { + b, err := json.Marshal(v) + if err != nil { + return err + } + + merged, err := runtime.JSONMerge(t.union, b) + t.union = merged + return err +} + +func (t FlagValue) MarshalJSON() ([]byte, error) { + b, err := t.union.MarshalJSON() + return b, err +} + +func (t *FlagValue) UnmarshalJSON(b []byte) error { + err := t.union.UnmarshalJSON(b) + return err +} + +// ServerInterface represents all server handlers. +type ServerInterface interface { + // lists all projects that have been configured for the dev server + // (GET /dev/projects) + GetDevProjects(w http.ResponseWriter, r *http.Request) + // remove the specified project from the dev server + // (DELETE /dev/projects/{projectKey}) + DeleteDevProjectsProjectKey(w http.ResponseWriter, r *http.Request, projectKey ProjectKey) + // get the specified project and its configuration for syncing from the LaunchDarkly Service + // (GET /dev/projects/{projectKey}) + GetDevProjectsProjectKey(w http.ResponseWriter, r *http.Request, projectKey ProjectKey) + // Add the project to the dev server + // (POST /dev/projects/{projectKey}) + PostDevProjectsProjectKey(w http.ResponseWriter, r *http.Request, projectKey ProjectKey) + // remove override for flag + // (DELETE /dev/projects/{projectKey}/overrides/{flagKey}) + DeleteDevProjectsProjectKeyOverridesFlagKey(w http.ResponseWriter, r *http.Request, projectKey ProjectKey, flagKey FlagKey) + // override flag value with value provided in the body + // (PUT /dev/projects/{projectKey}/overrides/{flagKey}) + PutDevProjectsProjectKeyOverridesFlagKey(w http.ResponseWriter, r *http.Request, projectKey ProjectKey, flagKey FlagKey) +} + +// ServerInterfaceWrapper converts contexts to parameters. +type ServerInterfaceWrapper struct { + Handler ServerInterface + HandlerMiddlewares []MiddlewareFunc + ErrorHandlerFunc func(w http.ResponseWriter, r *http.Request, err error) +} + +type MiddlewareFunc func(http.Handler) http.Handler + +// GetDevProjects operation middleware +func (siw *ServerInterfaceWrapper) GetDevProjects(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.GetDevProjects(w, r) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r.WithContext(ctx)) +} + +// DeleteDevProjectsProjectKey operation middleware +func (siw *ServerInterfaceWrapper) DeleteDevProjectsProjectKey(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + var err error + + // ------------- Path parameter "projectKey" ------------- + var projectKey ProjectKey + + err = runtime.BindStyledParameterWithOptions("simple", "projectKey", mux.Vars(r)["projectKey"], &projectKey, runtime.BindStyledParameterOptions{Explode: false, Required: true}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "projectKey", Err: err}) + return + } + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.DeleteDevProjectsProjectKey(w, r, projectKey) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r.WithContext(ctx)) +} + +// GetDevProjectsProjectKey operation middleware +func (siw *ServerInterfaceWrapper) GetDevProjectsProjectKey(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + var err error + + // ------------- Path parameter "projectKey" ------------- + var projectKey ProjectKey + + err = runtime.BindStyledParameterWithOptions("simple", "projectKey", mux.Vars(r)["projectKey"], &projectKey, runtime.BindStyledParameterOptions{Explode: false, Required: true}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "projectKey", Err: err}) + return + } + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.GetDevProjectsProjectKey(w, r, projectKey) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r.WithContext(ctx)) +} + +// PostDevProjectsProjectKey operation middleware +func (siw *ServerInterfaceWrapper) PostDevProjectsProjectKey(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + var err error + + // ------------- Path parameter "projectKey" ------------- + var projectKey ProjectKey + + err = runtime.BindStyledParameterWithOptions("simple", "projectKey", mux.Vars(r)["projectKey"], &projectKey, runtime.BindStyledParameterOptions{Explode: false, Required: true}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "projectKey", Err: err}) + return + } + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.PostDevProjectsProjectKey(w, r, projectKey) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r.WithContext(ctx)) +} + +// DeleteDevProjectsProjectKeyOverridesFlagKey operation middleware +func (siw *ServerInterfaceWrapper) DeleteDevProjectsProjectKeyOverridesFlagKey(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + var err error + + // ------------- Path parameter "projectKey" ------------- + var projectKey ProjectKey + + err = runtime.BindStyledParameterWithOptions("simple", "projectKey", mux.Vars(r)["projectKey"], &projectKey, runtime.BindStyledParameterOptions{Explode: false, Required: true}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "projectKey", Err: err}) + return + } + + // ------------- Path parameter "flagKey" ------------- + var flagKey FlagKey + + err = runtime.BindStyledParameterWithOptions("simple", "flagKey", mux.Vars(r)["flagKey"], &flagKey, runtime.BindStyledParameterOptions{Explode: false, Required: true}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "flagKey", Err: err}) + return + } + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.DeleteDevProjectsProjectKeyOverridesFlagKey(w, r, projectKey, flagKey) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r.WithContext(ctx)) +} + +// PutDevProjectsProjectKeyOverridesFlagKey operation middleware +func (siw *ServerInterfaceWrapper) PutDevProjectsProjectKeyOverridesFlagKey(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + var err error + + // ------------- Path parameter "projectKey" ------------- + var projectKey ProjectKey + + err = runtime.BindStyledParameterWithOptions("simple", "projectKey", mux.Vars(r)["projectKey"], &projectKey, runtime.BindStyledParameterOptions{Explode: false, Required: true}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "projectKey", Err: err}) + return + } + + // ------------- Path parameter "flagKey" ------------- + var flagKey FlagKey + + err = runtime.BindStyledParameterWithOptions("simple", "flagKey", mux.Vars(r)["flagKey"], &flagKey, runtime.BindStyledParameterOptions{Explode: false, Required: true}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "flagKey", Err: err}) + return + } + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.PutDevProjectsProjectKeyOverridesFlagKey(w, r, projectKey, flagKey) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r.WithContext(ctx)) +} + +type UnescapedCookieParamError struct { + ParamName string + Err error +} + +func (e *UnescapedCookieParamError) Error() string { + return fmt.Sprintf("error unescaping cookie parameter '%s'", e.ParamName) +} + +func (e *UnescapedCookieParamError) Unwrap() error { + return e.Err +} + +type UnmarshalingParamError struct { + ParamName string + Err error +} + +func (e *UnmarshalingParamError) Error() string { + return fmt.Sprintf("Error unmarshaling parameter %s as JSON: %s", e.ParamName, e.Err.Error()) +} + +func (e *UnmarshalingParamError) Unwrap() error { + return e.Err +} + +type RequiredParamError struct { + ParamName string +} + +func (e *RequiredParamError) Error() string { + return fmt.Sprintf("Query argument %s is required, but not found", e.ParamName) +} + +type RequiredHeaderError struct { + ParamName string + Err error +} + +func (e *RequiredHeaderError) Error() string { + return fmt.Sprintf("Header parameter %s is required, but not found", e.ParamName) +} + +func (e *RequiredHeaderError) Unwrap() error { + return e.Err +} + +type InvalidParamFormatError struct { + ParamName string + Err error +} + +func (e *InvalidParamFormatError) Error() string { + return fmt.Sprintf("Invalid format for parameter %s: %s", e.ParamName, e.Err.Error()) +} + +func (e *InvalidParamFormatError) Unwrap() error { + return e.Err +} + +type TooManyValuesForParamError struct { + ParamName string + Count int +} + +func (e *TooManyValuesForParamError) Error() string { + return fmt.Sprintf("Expected one value for %s, got %d", e.ParamName, e.Count) +} + +// Handler creates http.Handler with routing matching OpenAPI spec. +func Handler(si ServerInterface) http.Handler { + return HandlerWithOptions(si, GorillaServerOptions{}) +} + +type GorillaServerOptions struct { + BaseURL string + BaseRouter *mux.Router + Middlewares []MiddlewareFunc + ErrorHandlerFunc func(w http.ResponseWriter, r *http.Request, err error) +} + +// HandlerFromMux creates http.Handler with routing matching OpenAPI spec based on the provided mux. +func HandlerFromMux(si ServerInterface, r *mux.Router) http.Handler { + return HandlerWithOptions(si, GorillaServerOptions{ + BaseRouter: r, + }) +} + +func HandlerFromMuxWithBaseURL(si ServerInterface, r *mux.Router, baseURL string) http.Handler { + return HandlerWithOptions(si, GorillaServerOptions{ + BaseURL: baseURL, + BaseRouter: r, + }) +} + +// HandlerWithOptions creates http.Handler with additional options +func HandlerWithOptions(si ServerInterface, options GorillaServerOptions) http.Handler { + r := options.BaseRouter + + if r == nil { + r = mux.NewRouter() + } + if options.ErrorHandlerFunc == nil { + options.ErrorHandlerFunc = func(w http.ResponseWriter, r *http.Request, err error) { + http.Error(w, err.Error(), http.StatusBadRequest) + } + } + wrapper := ServerInterfaceWrapper{ + Handler: si, + HandlerMiddlewares: options.Middlewares, + ErrorHandlerFunc: options.ErrorHandlerFunc, + } + + r.HandleFunc(options.BaseURL+"/dev/projects", wrapper.GetDevProjects).Methods("GET") + + r.HandleFunc(options.BaseURL+"/dev/projects/{projectKey}", wrapper.DeleteDevProjectsProjectKey).Methods("DELETE") + + r.HandleFunc(options.BaseURL+"/dev/projects/{projectKey}", wrapper.GetDevProjectsProjectKey).Methods("GET") + + r.HandleFunc(options.BaseURL+"/dev/projects/{projectKey}", wrapper.PostDevProjectsProjectKey).Methods("POST") + + r.HandleFunc(options.BaseURL+"/dev/projects/{projectKey}/overrides/{flagKey}", wrapper.DeleteDevProjectsProjectKeyOverridesFlagKey).Methods("DELETE") + + r.HandleFunc(options.BaseURL+"/dev/projects/{projectKey}/overrides/{flagKey}", wrapper.PutDevProjectsProjectKeyOverridesFlagKey).Methods("PUT") + + return r +} + +type FlagOverrideJSONResponse struct { + // Override whether or not this is an overridden value or one from the source environment + Override bool `json:"override"` + + // Value value of a feature flag variation + Value FlagValue `json:"value"` +} + +type ProjectJSONResponse struct { + // LastSyncedFromSource unix timestamp for the lat time the flag values were synced from the source environment + LastSyncedFromSource int `json:"_lastSyncedFromSource"` + + // Context context object to use when evaluating flags in source environment + Context map[string]interface{} `json:"context"` + + // SourceEnvironmentKey environment to copy flag values from + SourceEnvironmentKey string `json:"sourceEnvironmentKey"` +} + +type GetDevProjectsRequestObject struct { +} + +type GetDevProjectsResponseObject interface { + VisitGetDevProjectsResponse(w http.ResponseWriter) error +} + +type GetDevProjects200JSONResponse []string + +func (response GetDevProjects200JSONResponse) VisitGetDevProjectsResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type DeleteDevProjectsProjectKeyRequestObject struct { + ProjectKey ProjectKey `json:"projectKey"` +} + +type DeleteDevProjectsProjectKeyResponseObject interface { + VisitDeleteDevProjectsProjectKeyResponse(w http.ResponseWriter) error +} + +type DeleteDevProjectsProjectKey204Response struct { +} + +func (response DeleteDevProjectsProjectKey204Response) VisitDeleteDevProjectsProjectKeyResponse(w http.ResponseWriter) error { + w.WriteHeader(204) + return nil +} + +type GetDevProjectsProjectKeyRequestObject struct { + ProjectKey ProjectKey `json:"projectKey"` +} + +type GetDevProjectsProjectKeyResponseObject interface { + VisitGetDevProjectsProjectKeyResponse(w http.ResponseWriter) error +} + +type GetDevProjectsProjectKey200JSONResponse struct{ ProjectJSONResponse } + +func (response GetDevProjectsProjectKey200JSONResponse) VisitGetDevProjectsProjectKeyResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type PostDevProjectsProjectKeyRequestObject struct { + ProjectKey ProjectKey `json:"projectKey"` + Body *PostDevProjectsProjectKeyJSONRequestBody +} + +type PostDevProjectsProjectKeyResponseObject interface { + VisitPostDevProjectsProjectKeyResponse(w http.ResponseWriter) error +} + +type PostDevProjectsProjectKey201JSONResponse struct{ ProjectJSONResponse } + +func (response PostDevProjectsProjectKey201JSONResponse) VisitPostDevProjectsProjectKeyResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(201) + + return json.NewEncoder(w).Encode(response) +} + +type DeleteDevProjectsProjectKeyOverridesFlagKeyRequestObject struct { + ProjectKey ProjectKey `json:"projectKey"` + FlagKey FlagKey `json:"flagKey"` +} + +type DeleteDevProjectsProjectKeyOverridesFlagKeyResponseObject interface { + VisitDeleteDevProjectsProjectKeyOverridesFlagKeyResponse(w http.ResponseWriter) error +} + +type DeleteDevProjectsProjectKeyOverridesFlagKey200JSONResponse struct{ FlagOverrideJSONResponse } + +func (response DeleteDevProjectsProjectKeyOverridesFlagKey200JSONResponse) VisitDeleteDevProjectsProjectKeyOverridesFlagKeyResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type PutDevProjectsProjectKeyOverridesFlagKeyRequestObject struct { + ProjectKey ProjectKey `json:"projectKey"` + FlagKey FlagKey `json:"flagKey"` + Body *PutDevProjectsProjectKeyOverridesFlagKeyJSONRequestBody +} + +type PutDevProjectsProjectKeyOverridesFlagKeyResponseObject interface { + VisitPutDevProjectsProjectKeyOverridesFlagKeyResponse(w http.ResponseWriter) error +} + +type PutDevProjectsProjectKeyOverridesFlagKey200JSONResponse struct{ FlagOverrideJSONResponse } + +func (response PutDevProjectsProjectKeyOverridesFlagKey200JSONResponse) VisitPutDevProjectsProjectKeyOverridesFlagKeyResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +// StrictServerInterface represents all server handlers. +type StrictServerInterface interface { + // lists all projects that have been configured for the dev server + // (GET /dev/projects) + GetDevProjects(ctx context.Context, request GetDevProjectsRequestObject) (GetDevProjectsResponseObject, error) + // remove the specified project from the dev server + // (DELETE /dev/projects/{projectKey}) + DeleteDevProjectsProjectKey(ctx context.Context, request DeleteDevProjectsProjectKeyRequestObject) (DeleteDevProjectsProjectKeyResponseObject, error) + // get the specified project and its configuration for syncing from the LaunchDarkly Service + // (GET /dev/projects/{projectKey}) + GetDevProjectsProjectKey(ctx context.Context, request GetDevProjectsProjectKeyRequestObject) (GetDevProjectsProjectKeyResponseObject, error) + // Add the project to the dev server + // (POST /dev/projects/{projectKey}) + PostDevProjectsProjectKey(ctx context.Context, request PostDevProjectsProjectKeyRequestObject) (PostDevProjectsProjectKeyResponseObject, error) + // remove override for flag + // (DELETE /dev/projects/{projectKey}/overrides/{flagKey}) + DeleteDevProjectsProjectKeyOverridesFlagKey(ctx context.Context, request DeleteDevProjectsProjectKeyOverridesFlagKeyRequestObject) (DeleteDevProjectsProjectKeyOverridesFlagKeyResponseObject, error) + // override flag value with value provided in the body + // (PUT /dev/projects/{projectKey}/overrides/{flagKey}) + PutDevProjectsProjectKeyOverridesFlagKey(ctx context.Context, request PutDevProjectsProjectKeyOverridesFlagKeyRequestObject) (PutDevProjectsProjectKeyOverridesFlagKeyResponseObject, error) +} + +type StrictHandlerFunc = strictnethttp.StrictHTTPHandlerFunc +type StrictMiddlewareFunc = strictnethttp.StrictHTTPMiddlewareFunc + +type StrictHTTPServerOptions struct { + RequestErrorHandlerFunc func(w http.ResponseWriter, r *http.Request, err error) + ResponseErrorHandlerFunc func(w http.ResponseWriter, r *http.Request, err error) +} + +func NewStrictHandler(ssi StrictServerInterface, middlewares []StrictMiddlewareFunc) ServerInterface { + return &strictHandler{ssi: ssi, middlewares: middlewares, options: StrictHTTPServerOptions{ + RequestErrorHandlerFunc: func(w http.ResponseWriter, r *http.Request, err error) { + http.Error(w, err.Error(), http.StatusBadRequest) + }, + ResponseErrorHandlerFunc: func(w http.ResponseWriter, r *http.Request, err error) { + http.Error(w, err.Error(), http.StatusInternalServerError) + }, + }} +} + +func NewStrictHandlerWithOptions(ssi StrictServerInterface, middlewares []StrictMiddlewareFunc, options StrictHTTPServerOptions) ServerInterface { + return &strictHandler{ssi: ssi, middlewares: middlewares, options: options} +} + +type strictHandler struct { + ssi StrictServerInterface + middlewares []StrictMiddlewareFunc + options StrictHTTPServerOptions +} + +// GetDevProjects operation middleware +func (sh *strictHandler) GetDevProjects(w http.ResponseWriter, r *http.Request) { + var request GetDevProjectsRequestObject + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.GetDevProjects(ctx, request.(GetDevProjectsRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "GetDevProjects") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(GetDevProjectsResponseObject); ok { + if err := validResponse.VisitGetDevProjectsResponse(w); err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } + } else if response != nil { + sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response)) + } +} + +// DeleteDevProjectsProjectKey operation middleware +func (sh *strictHandler) DeleteDevProjectsProjectKey(w http.ResponseWriter, r *http.Request, projectKey ProjectKey) { + var request DeleteDevProjectsProjectKeyRequestObject + + request.ProjectKey = projectKey + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.DeleteDevProjectsProjectKey(ctx, request.(DeleteDevProjectsProjectKeyRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "DeleteDevProjectsProjectKey") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(DeleteDevProjectsProjectKeyResponseObject); ok { + if err := validResponse.VisitDeleteDevProjectsProjectKeyResponse(w); err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } + } else if response != nil { + sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response)) + } +} + +// GetDevProjectsProjectKey operation middleware +func (sh *strictHandler) GetDevProjectsProjectKey(w http.ResponseWriter, r *http.Request, projectKey ProjectKey) { + var request GetDevProjectsProjectKeyRequestObject + + request.ProjectKey = projectKey + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.GetDevProjectsProjectKey(ctx, request.(GetDevProjectsProjectKeyRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "GetDevProjectsProjectKey") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(GetDevProjectsProjectKeyResponseObject); ok { + if err := validResponse.VisitGetDevProjectsProjectKeyResponse(w); err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } + } else if response != nil { + sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response)) + } +} + +// PostDevProjectsProjectKey operation middleware +func (sh *strictHandler) PostDevProjectsProjectKey(w http.ResponseWriter, r *http.Request, projectKey ProjectKey) { + var request PostDevProjectsProjectKeyRequestObject + + request.ProjectKey = projectKey + + var body PostDevProjectsProjectKeyJSONRequestBody + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't decode JSON body: %w", err)) + return + } + request.Body = &body + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.PostDevProjectsProjectKey(ctx, request.(PostDevProjectsProjectKeyRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "PostDevProjectsProjectKey") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(PostDevProjectsProjectKeyResponseObject); ok { + if err := validResponse.VisitPostDevProjectsProjectKeyResponse(w); err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } + } else if response != nil { + sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response)) + } +} + +// DeleteDevProjectsProjectKeyOverridesFlagKey operation middleware +func (sh *strictHandler) DeleteDevProjectsProjectKeyOverridesFlagKey(w http.ResponseWriter, r *http.Request, projectKey ProjectKey, flagKey FlagKey) { + var request DeleteDevProjectsProjectKeyOverridesFlagKeyRequestObject + + request.ProjectKey = projectKey + request.FlagKey = flagKey + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.DeleteDevProjectsProjectKeyOverridesFlagKey(ctx, request.(DeleteDevProjectsProjectKeyOverridesFlagKeyRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "DeleteDevProjectsProjectKeyOverridesFlagKey") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(DeleteDevProjectsProjectKeyOverridesFlagKeyResponseObject); ok { + if err := validResponse.VisitDeleteDevProjectsProjectKeyOverridesFlagKeyResponse(w); err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } + } else if response != nil { + sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response)) + } +} + +// PutDevProjectsProjectKeyOverridesFlagKey operation middleware +func (sh *strictHandler) PutDevProjectsProjectKeyOverridesFlagKey(w http.ResponseWriter, r *http.Request, projectKey ProjectKey, flagKey FlagKey) { + var request PutDevProjectsProjectKeyOverridesFlagKeyRequestObject + + request.ProjectKey = projectKey + request.FlagKey = flagKey + + var body PutDevProjectsProjectKeyOverridesFlagKeyJSONRequestBody + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't decode JSON body: %w", err)) + return + } + request.Body = &body + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.PutDevProjectsProjectKeyOverridesFlagKey(ctx, request.(PutDevProjectsProjectKeyOverridesFlagKeyRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "PutDevProjectsProjectKeyOverridesFlagKey") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(PutDevProjectsProjectKeyOverridesFlagKeyResponseObject); ok { + if err := validResponse.VisitPutDevProjectsProjectKeyOverridesFlagKeyResponse(w); err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } + } else if response != nil { + sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response)) + } +} diff --git a/internal/dev_server/api/server.go b/internal/dev_server/api/server.go new file mode 100644 index 00000000..f183e8a9 --- /dev/null +++ b/internal/dev_server/api/server.go @@ -0,0 +1,39 @@ +package api + +import "context" + +type Server struct{} + +func NewStrictServer() Server { + return Server{} +} + +func (s Server) GetDevProjects(ctx context.Context, request GetDevProjectsRequestObject) (GetDevProjectsResponseObject, error) { + //TODO implement me + panic("implement me") +} + +func (s Server) DeleteDevProjectsProjectKey(ctx context.Context, request DeleteDevProjectsProjectKeyRequestObject) (DeleteDevProjectsProjectKeyResponseObject, error) { + //TODO implement me + panic("implement me") +} + +func (s Server) GetDevProjectsProjectKey(ctx context.Context, request GetDevProjectsProjectKeyRequestObject) (GetDevProjectsProjectKeyResponseObject, error) { + //TODO implement me + panic("implement me") +} + +func (s Server) PostDevProjectsProjectKey(ctx context.Context, request PostDevProjectsProjectKeyRequestObject) (PostDevProjectsProjectKeyResponseObject, error) { + //TODO implement me + panic("implement me") +} + +func (s Server) DeleteDevProjectsProjectKeyOverridesFlagKey(ctx context.Context, request DeleteDevProjectsProjectKeyOverridesFlagKeyRequestObject) (DeleteDevProjectsProjectKeyOverridesFlagKeyResponseObject, error) { + //TODO implement me + panic("implement me") +} + +func (s Server) PutDevProjectsProjectKeyOverridesFlagKey(ctx context.Context, request PutDevProjectsProjectKeyOverridesFlagKeyRequestObject) (PutDevProjectsProjectKeyOverridesFlagKeyResponseObject, error) { + //TODO implement me + panic("implement me") +} diff --git a/internal/dev_server/main.go b/internal/dev_server/main.go new file mode 100644 index 00000000..ef9468d0 --- /dev/null +++ b/internal/dev_server/main.go @@ -0,0 +1,24 @@ +package main + +import ( + "log" + "net/http" + + "github.com/gorilla/mux" + "github.com/launchdarkly/ldcli/internal/dev_server/api" +) + +// FIXME this is gonna get replaced by a cli command +func main() { + ss := api.NewStrictServer() + apiServer := api.NewStrictHandler(ss, nil) + r := mux.NewRouter() + // TODO need a subrouter for relay endpoints + // TODO need to wire up sqlite + handler := api.HandlerFromMux(apiServer, r) + server := http.Server{ + Addr: "0.0.0.0:8765", + Handler: handler, + } + log.Fatal(server.ListenAndServe()) +} diff --git a/tools.go b/tools.go index 06ab7d0f..8c05d7c1 100644 --- a/tools.go +++ b/tools.go @@ -1 +1,3 @@ package main + +import _ "github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen" diff --git a/vendor/github.com/apapsch/go-jsonmerge/v2/.editorconfig b/vendor/github.com/apapsch/go-jsonmerge/v2/.editorconfig new file mode 100644 index 00000000..fbb2c8f3 --- /dev/null +++ b/vendor/github.com/apapsch/go-jsonmerge/v2/.editorconfig @@ -0,0 +1,6 @@ +[*] +end_of_line = lf +insert_final_newline = true + +[*.{cmd,bat}] +end_of_line = crlf diff --git a/vendor/github.com/apapsch/go-jsonmerge/v2/.gitattributes b/vendor/github.com/apapsch/go-jsonmerge/v2/.gitattributes new file mode 100644 index 00000000..9be55028 --- /dev/null +++ b/vendor/github.com/apapsch/go-jsonmerge/v2/.gitattributes @@ -0,0 +1,175 @@ +## AUTO-DETECT - Handle line endings automatically for files detected +## as text and leave all files detected as binary untouched. +## This will handle all files NOT defined below. +* text=auto + +# Custom for Visual Studio +*.sln text eol=crlf +*.csproj text eol=crlf +*.vbproj text eol=crlf +*.fsproj text eol=crlf +*.dbproj text eol=crlf + +*.vcxproj text eol=crlf +*.vcxitems text eol=crlf +*.props text eol=crlf +*.filters text eol=crlf + +# Documents +*.doc diff=astextplain +*.DOC diff=astextplain +*.docx diff=astextplain +*.DOCX diff=astextplain +*.dot diff=astextplain +*.DOT diff=astextplain +*.pdf diff=astextplain +*.PDF diff=astextplain +*.rtf diff=astextplain +*.RTF diff=astextplain +*.csv text +*.sql text +*.ini text + +## SOURCE CODE +*.go text eol=lf +*.c text eol=lf +*.h text eol=lf +*.bat text eol=crlf +*.cmd text eol=crlf +*.coffee text eol=lf + +*.htm text diff=html +*.html text diff=html +*.xml text diff=html +*.xhtml text diff=html + +*.js text eol=lf +*.jsx text eol=lf +*.json text eol=lf +*.ts text eol=lf + +*.css text diff=css eol=lf +*.scss text diff=css eol=lf +*.less text diff=css eol=lf +*.sass text eol=lf + +*.sh text eol=lf + +## DOCUMENTATION +*.md text eol=lf +*.txt text +AUTHORS text eol=lf +CHANGELOG text eol=lf +CHANGES text eol=lf +CONTRIBUTING text eol=lf +COPYING text eol=lf +INSTALL text eol=lf +license text eol=lf +LICENSE text eol=lf +NEWS text eol=lf +readme text eol=lf +*README* text eol=lf +TODO text eol=lf + +## TEMPLATES +*.dot text +*.ejs text +*.haml text +*.handlebars text +*.hbs text +*.hbt text +*.jade text +*.latte text +*.mustache text +*.tmpl text + +## LINTERS +.csslintrc text eol=lf +.eslintrc text eol=lf +.jscsrc text eol=lf +.jshintrc text eol=lf +.jshintignore text eol=lf +.stylelintrc text eol=lf + +## CONFIGS +*.bowerrc text eol=lf +*.cnf text +*.conf text +*.config text +.editorconfig text eol=lf +.gitattributes text eol=lf +.gitconfig text eol=lf +.gitignore text eol=lf +*.npmignore text eol=lf +*.yaml text eol=lf +*.yml text eol=lf +Makefile text eol=lf +makefile text eol=lf + +## GRAPHICS +*.ai binary +*.bmp binary +*.eps binary +*.gif binary +*.ico binary +*.jng binary +*.jp2 binary +*.jpg binary +*.jpeg binary +*.jpx binary +*.jxr binary +*.pdf binary +*.png binary +*.psb binary +*.psd binary +*.svg text +*.svgz binary +*.tif binary +*.tiff binary +*.wbmp binary +*.webp binary + +## AUDIO +*.kar binary +*.m4a binary +*.mid binary +*.midi binary +*.mp3 binary +*.ogg binary +*.ra binary + +## VIDEO +*.3gpp binary +*.3gp binary +*.as binary +*.asf binary +*.asx binary +*.fla binary +*.flv binary +*.m4v binary +*.mng binary +*.mov binary +*.mp4 binary +*.mpeg binary +*.mpg binary +*.swc binary +*.swf binary +*.webm binary + +## ARCHIVES +*.7z binary +*.gz binary +*.rar binary +*.tar binary +*.zip binary + +## FONTS +*.ttf binary +*.eot binary +*.otf binary +*.woff binary +*.woff2 binary + +## EXECUTABLES +*.exe binary +*.dll binary diff --git a/vendor/github.com/apapsch/go-jsonmerge/v2/.gitignore b/vendor/github.com/apapsch/go-jsonmerge/v2/.gitignore new file mode 100644 index 00000000..5efe425b --- /dev/null +++ b/vendor/github.com/apapsch/go-jsonmerge/v2/.gitignore @@ -0,0 +1,11 @@ +# GoLand +/.idea/ + +/vendor/ + +/cmd/cmd.exe +/cmd/cmd + +/artifacts/ +/test/ +/cmd/test/ diff --git a/vendor/github.com/apapsch/go-jsonmerge/v2/.gitlab-ci.yml b/vendor/github.com/apapsch/go-jsonmerge/v2/.gitlab-ci.yml new file mode 100644 index 00000000..a0d178f9 --- /dev/null +++ b/vendor/github.com/apapsch/go-jsonmerge/v2/.gitlab-ci.yml @@ -0,0 +1,42 @@ +variables: + GOPROJ: "github.com/RaveNoX/go-jsonmerge" + + +stages: +- test +- build + +test: + tags: + - docker + - linux + image: golang:latest + stage: test + script: + - mkdir -p artifacts + - go test -cover -v -coverprofile="./artifacts/cover.out" ./ + - go tool cover -html="./artifacts/cover.out" -o "./artifacts/cover.htm" + - go test -cover -v -coverprofile="./artifacts/cover_cmd.out" ./cmd/jsonmerge + - go tool cover -html="./artifacts/cover_cmd.out" -o "./artifacts/cover_cmd.htm" + artifacts: + paths: + - artifacts/* + +build: + stage: build + tags: + - docker + - linux + image: golang:latest + script: + - mkdir -p artifacts + - echo "Building for Linux" + - GOOS=linux GOARCH=amd64 go build -o artifacts/jsonmerge ./cmd/jsonmerge + - echo "Building for MacOS (darwin)" + - GOOS=darwin GOARCH=amd64 go build -o artifacts/jsonmerge_darwin ./cmd/jsonmerge + - echo "Building for Windows" + - GOOS=windows GOARCH=amd64 go build -o artifacts/jsonmerge.exe ./cmd/jsonmerge + artifacts: + paths: + - artifacts/* + diff --git a/vendor/github.com/apapsch/go-jsonmerge/v2/.travis.yml b/vendor/github.com/apapsch/go-jsonmerge/v2/.travis.yml new file mode 100644 index 00000000..eae1a8a2 --- /dev/null +++ b/vendor/github.com/apapsch/go-jsonmerge/v2/.travis.yml @@ -0,0 +1,19 @@ +language: go + +go: +- 1.x + +install: +- mkdir -p artifacts + +env: + - GO111MODULE=on + +script: +- go test -cover -v -coverprofile="./artifacts/cover.out" ./ +- go tool cover -html="./artifacts/cover.out" -o "./artifacts/cover.htm" +- go test -cover -v -coverprofile="./artifacts/cover_cmd.out" ./cmd/jsonmerge +- go tool cover -html="./artifacts/cover_cmd.out" -o "./artifacts/cover_cmd.htm" +- GOARCH=amd64 GOOS=linux go build -o artifacts/jsonmerge ./cmd/jsonmerge +- GOARCH=amd64 GOOS=windows go build -o artifacts/jsonmerge.exe ./cmd/jsonmerge +- GOARCH=amd64 GOOS=darwin go build -o artifacts/jsonmerge_darwin ./cmd/jsonmerge diff --git a/vendor/github.com/apapsch/go-jsonmerge/v2/LICENSE b/vendor/github.com/apapsch/go-jsonmerge/v2/LICENSE new file mode 100644 index 00000000..f23057e4 --- /dev/null +++ b/vendor/github.com/apapsch/go-jsonmerge/v2/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2016-2019 Artur Kraev + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/apapsch/go-jsonmerge/v2/README.md b/vendor/github.com/apapsch/go-jsonmerge/v2/README.md new file mode 100644 index 00000000..e11bd4c4 --- /dev/null +++ b/vendor/github.com/apapsch/go-jsonmerge/v2/README.md @@ -0,0 +1,81 @@ +# go-jsonmerge +[![Build Status](https://travis-ci.org/RaveNoX/go-jsonmerge.svg?branch=master)](https://travis-ci.org/RaveNoX/go-jsonmerge) +[![GoDoc](https://godoc.org/github.com/RaveNoX/go-jsonmerge?status.svg)](https://godoc.org/github.com/RaveNoX/go-jsonmerge) + +GO library for merging JSON objects + +## Original document +```json +{ + "number": 1, + "string": "value", + "object": { + "number": 1, + "string": "value", + "nested object": { + "number": 2 + }, + "array": [1, 2, 3], + "partial_array": [1, 2, 3] + } +} +``` + +## Patch +```json +{ + "number": 2, + "string": "value1", + "nonexitent": "woot", + "object": { + "number": 3, + "string": "value2", + "nested object": { + "number": 4 + }, + "array": [3, 2, 1], + "partial_array": { + "1": 4 + } + } +} +``` + +## Result +```json +{ + "number": 2, + "string": "value1", + "object": { + "number": 3, + "string": "value2", + "nested object": { + "number": 4 + }, + "array": [3, 2, 1], + "partial_array": [1, 4, 3] + } +} +``` + +## Commandline Tool + +```bash +$ go get -u github.com/RaveNoX/go-jsonmerge/cmd/jsonmerge +$ jsonmerge [options] ... +# For help +$ jsonmerge -h +``` + +## Development +``` +# Install depencencies +./init.sh + +# Build +./build.sh +``` + + +## License +[MIT](./LICENSE.MD) diff --git a/vendor/github.com/apapsch/go-jsonmerge/v2/build.cmd b/vendor/github.com/apapsch/go-jsonmerge/v2/build.cmd new file mode 100644 index 00000000..ebd98767 --- /dev/null +++ b/vendor/github.com/apapsch/go-jsonmerge/v2/build.cmd @@ -0,0 +1,25 @@ +@ECHO OFF +setlocal + +set GOARCH=amd64 + +cd %~dp0 +md artifacts + +echo Windows +set GOOS=windows +call go build -o artifacts\jsonmerge.exe .\cmd || goto :error + +echo Linux +set GOOS=linux +call go build -o artifacts\jsonmerge .\cmd || goto :error + +echo Darwin +set GOOS=darwin +call go build -o artifacts\jsonmerge_darwin .\cmd || goto :error + +echo Build done +exit + +:error +exit /b %errorlevel% diff --git a/vendor/github.com/apapsch/go-jsonmerge/v2/build.sh b/vendor/github.com/apapsch/go-jsonmerge/v2/build.sh new file mode 100644 index 00000000..29be8924 --- /dev/null +++ b/vendor/github.com/apapsch/go-jsonmerge/v2/build.sh @@ -0,0 +1,19 @@ +#!/bin/sh + +set -e + +MY_DIR=$(dirname "$0") + +cd "${MY_DIR}" +mkdir -p "artifacts" + +echo "Linux" +GOARCH=amd64 GOOS=linux go build -o "artifacts/jsonmerge" ./cmd + +echo "Windows" +GOARCH=amd64 GOOS=windows go build -o "artifacts/jsonmerge.exe" ./cmd + +echo "Mac(darwin)" +GOARCH=amd64 GOOS=darwin go build -o "artifacts/jsonmerge_darwin" ./cmd + +echo "Build done" diff --git a/vendor/github.com/apapsch/go-jsonmerge/v2/doc.go b/vendor/github.com/apapsch/go-jsonmerge/v2/doc.go new file mode 100644 index 00000000..5bd6e15e --- /dev/null +++ b/vendor/github.com/apapsch/go-jsonmerge/v2/doc.go @@ -0,0 +1,52 @@ +// Package jsonmerge helps mergeing JSON objects +// +// For example you have this documents: +// +// original.json +// { +// "number": 1, +// "string": "value", +// "object": { +// "number": 1, +// "string": "value", +// "nested object": { +// "number": 2 +// }, +// "array": [1, 2, 3], +// "partial_array": [1, 2, 3] +// } +// } +// +// patch.json +// { +// "number": 2, +// "string": "value1", +// "nonexitent": "woot", +// "object": { +// "number": 3, +// "string": "value2", +// "nested object": { +// "number": 4 +// }, +// "array": [3, 2, 1], +// "partial_array": { +// "1": 4 +// } +// } +// } +// +// After merge you will have this result: +// { +// "number": 2, +// "string": "value1", +// "object": { +// "number": 3, +// "string": "value2", +// "nested object": { +// "number": 4 +// }, +// "array": [3, 2, 1], +// "partial_array": [1, 4, 3] +// } +// } +package jsonmerge diff --git a/vendor/github.com/apapsch/go-jsonmerge/v2/merge.go b/vendor/github.com/apapsch/go-jsonmerge/v2/merge.go new file mode 100644 index 00000000..8ba052f1 --- /dev/null +++ b/vendor/github.com/apapsch/go-jsonmerge/v2/merge.go @@ -0,0 +1,167 @@ +package jsonmerge + +import ( + "bytes" + "encoding/json" + "fmt" + "reflect" + "strconv" + "strings" +) + +// Merger describes result of merge operation and provides +// configuration. +type Merger struct { + // Errors is slice of non-critical errors of merge operations + Errors []error + // Replaced is describe replacements + // Key is path in document like + // "prop1.prop2.prop3" for object properties or + // "arr1.1.prop" for arrays + // Value is value of replacemet + Replaced map[string]interface{} + // CopyNonexistent enables setting fields into the result + // which only exist in the patch. + CopyNonexistent bool +} + +func (m *Merger) mergeValue(path []string, patch map[string]interface{}, key string, value interface{}) interface{} { + patchValue, patchHasValue := patch[key] + + if !patchHasValue { + return value + } + + _, patchValueIsObject := patchValue.(map[string]interface{}) + + path = append(path, key) + pathStr := strings.Join(path, ".") + + if _, ok := value.(map[string]interface{}); ok { + if !patchValueIsObject { + err := fmt.Errorf("patch value must be object for key \"%v\"", pathStr) + m.Errors = append(m.Errors, err) + return value + } + + return m.mergeObjects(value, patchValue, path) + } + + if _, ok := value.([]interface{}); ok && patchValueIsObject { + return m.mergeObjects(value, patchValue, path) + } + + if !reflect.DeepEqual(value, patchValue) { + m.Replaced[pathStr] = patchValue + } + + return patchValue +} + +func (m *Merger) mergeObjects(data, patch interface{}, path []string) interface{} { + if patchObject, ok := patch.(map[string]interface{}); ok { + if dataArray, ok := data.([]interface{}); ok { + ret := make([]interface{}, len(dataArray)) + + for i, val := range dataArray { + ret[i] = m.mergeValue(path, patchObject, strconv.Itoa(i), val) + } + + return ret + } else if dataObject, ok := data.(map[string]interface{}); ok { + ret := make(map[string]interface{}) + + for k, v := range dataObject { + ret[k] = m.mergeValue(path, patchObject, k, v) + } + if m.CopyNonexistent { + for k, v := range patchObject { + if _, ok := dataObject[k]; !ok { + ret[k] = v + } + } + } + + return ret + } + } + + return data +} + +// Merge merges patch document to data document +// +// Returning merged document. Result of merge operation can be +// obtained from the Merger. Result information is discarded before +// merging. +func (m *Merger) Merge(data, patch interface{}) interface{} { + m.Replaced = make(map[string]interface{}) + m.Errors = make([]error, 0) + return m.mergeObjects(data, patch, nil) +} + +// MergeBytesIndent merges patch document buffer to data document buffer +// +// Use prefix and indent for set indentation like in json.MarshalIndent +// +// Returning merged document buffer and error if any. +func (m *Merger) MergeBytesIndent(dataBuff, patchBuff []byte, prefix, indent string) (mergedBuff []byte, err error) { + var data, patch, merged interface{} + + err = unmarshalJSON(dataBuff, &data) + if err != nil { + err = fmt.Errorf("error in data JSON: %v", err) + return + } + + err = unmarshalJSON(patchBuff, &patch) + if err != nil { + err = fmt.Errorf("error in patch JSON: %v", err) + return + } + + merged = m.Merge(data, patch) + + mergedBuff, err = json.MarshalIndent(merged, prefix, indent) + if err != nil { + err = fmt.Errorf("error writing merged JSON: %v", err) + } + + return +} + +// MergeBytes merges patch document buffer to data document buffer +// +// Returning merged document buffer, merge info and +// error if any +func (m *Merger) MergeBytes(dataBuff, patchBuff []byte) (mergedBuff []byte, err error) { + var data, patch, merged interface{} + + err = unmarshalJSON(dataBuff, &data) + if err != nil { + err = fmt.Errorf("error in data JSON: %v", err) + return + } + + err = unmarshalJSON(patchBuff, &patch) + if err != nil { + err = fmt.Errorf("error in patch JSON: %v", err) + return + } + + merged = m.Merge(data, patch) + + mergedBuff, err = json.Marshal(merged) + if err != nil { + err = fmt.Errorf("error writing merged JSON: %v", err) + } + + return +} + +func unmarshalJSON(buff []byte, data interface{}) error { + decoder := json.NewDecoder(bytes.NewReader(buff)) + decoder.UseNumber() + + return decoder.Decode(data) +} diff --git a/vendor/github.com/gorilla/mux/.editorconfig b/vendor/github.com/gorilla/mux/.editorconfig new file mode 100644 index 00000000..c6b74c3e --- /dev/null +++ b/vendor/github.com/gorilla/mux/.editorconfig @@ -0,0 +1,20 @@ +; https://editorconfig.org/ + +root = true + +[*] +insert_final_newline = true +charset = utf-8 +trim_trailing_whitespace = true +indent_style = space +indent_size = 2 + +[{Makefile,go.mod,go.sum,*.go,.gitmodules}] +indent_style = tab +indent_size = 4 + +[*.md] +indent_size = 4 +trim_trailing_whitespace = false + +eclint_indent_style = unset \ No newline at end of file diff --git a/vendor/github.com/gorilla/mux/.gitignore b/vendor/github.com/gorilla/mux/.gitignore new file mode 100644 index 00000000..84039fec --- /dev/null +++ b/vendor/github.com/gorilla/mux/.gitignore @@ -0,0 +1 @@ +coverage.coverprofile diff --git a/vendor/github.com/gorilla/mux/LICENSE b/vendor/github.com/gorilla/mux/LICENSE new file mode 100644 index 00000000..bb9d80bc --- /dev/null +++ b/vendor/github.com/gorilla/mux/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2023 The Gorilla Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/github.com/gorilla/mux/Makefile b/vendor/github.com/gorilla/mux/Makefile new file mode 100644 index 00000000..98f5ab75 --- /dev/null +++ b/vendor/github.com/gorilla/mux/Makefile @@ -0,0 +1,34 @@ +GO_LINT=$(shell which golangci-lint 2> /dev/null || echo '') +GO_LINT_URI=github.com/golangci/golangci-lint/cmd/golangci-lint@latest + +GO_SEC=$(shell which gosec 2> /dev/null || echo '') +GO_SEC_URI=github.com/securego/gosec/v2/cmd/gosec@latest + +GO_VULNCHECK=$(shell which govulncheck 2> /dev/null || echo '') +GO_VULNCHECK_URI=golang.org/x/vuln/cmd/govulncheck@latest + +.PHONY: golangci-lint +golangci-lint: + $(if $(GO_LINT), ,go install $(GO_LINT_URI)) + @echo "##### Running golangci-lint" + golangci-lint run -v + +.PHONY: gosec +gosec: + $(if $(GO_SEC), ,go install $(GO_SEC_URI)) + @echo "##### Running gosec" + gosec ./... + +.PHONY: govulncheck +govulncheck: + $(if $(GO_VULNCHECK), ,go install $(GO_VULNCHECK_URI)) + @echo "##### Running govulncheck" + govulncheck ./... + +.PHONY: verify +verify: golangci-lint gosec govulncheck + +.PHONY: test +test: + @echo "##### Running tests" + go test -race -cover -coverprofile=coverage.coverprofile -covermode=atomic -v ./... \ No newline at end of file diff --git a/vendor/github.com/gorilla/mux/README.md b/vendor/github.com/gorilla/mux/README.md new file mode 100644 index 00000000..382513d5 --- /dev/null +++ b/vendor/github.com/gorilla/mux/README.md @@ -0,0 +1,812 @@ +# gorilla/mux + +![testing](https://github.com/gorilla/mux/actions/workflows/test.yml/badge.svg) +[![codecov](https://codecov.io/github/gorilla/mux/branch/main/graph/badge.svg)](https://codecov.io/github/gorilla/mux) +[![godoc](https://godoc.org/github.com/gorilla/mux?status.svg)](https://godoc.org/github.com/gorilla/mux) +[![sourcegraph](https://sourcegraph.com/github.com/gorilla/mux/-/badge.svg)](https://sourcegraph.com/github.com/gorilla/mux?badge) + + +![Gorilla Logo](https://github.com/gorilla/.github/assets/53367916/d92caabf-98e0-473e-bfbf-ab554ba435e5) + +Package `gorilla/mux` implements a request router and dispatcher for matching incoming requests to +their respective handler. + +The name mux stands for "HTTP request multiplexer". Like the standard `http.ServeMux`, `mux.Router` matches incoming requests against a list of registered routes and calls a handler for the route that matches the URL or other conditions. The main features are: + +* It implements the `http.Handler` interface so it is compatible with the standard `http.ServeMux`. +* Requests can be matched based on URL host, path, path prefix, schemes, header and query values, HTTP methods or using custom matchers. +* URL hosts, paths and query values can have variables with an optional regular expression. +* Registered URLs can be built, or "reversed", which helps maintaining references to resources. +* Routes can be used as subrouters: nested routes are only tested if the parent route matches. This is useful to define groups of routes that share common conditions like a host, a path prefix or other repeated attributes. As a bonus, this optimizes request matching. + +--- + +* [Install](#install) +* [Examples](#examples) +* [Matching Routes](#matching-routes) +* [Static Files](#static-files) +* [Serving Single Page Applications](#serving-single-page-applications) (e.g. React, Vue, Ember.js, etc.) +* [Registered URLs](#registered-urls) +* [Walking Routes](#walking-routes) +* [Graceful Shutdown](#graceful-shutdown) +* [Middleware](#middleware) +* [Handling CORS Requests](#handling-cors-requests) +* [Testing Handlers](#testing-handlers) +* [Full Example](#full-example) + +--- + +## Install + +With a [correctly configured](https://golang.org/doc/install#testing) Go toolchain: + +```sh +go get -u github.com/gorilla/mux +``` + +## Examples + +Let's start registering a couple of URL paths and handlers: + +```go +func main() { + r := mux.NewRouter() + r.HandleFunc("/", HomeHandler) + r.HandleFunc("/products", ProductsHandler) + r.HandleFunc("/articles", ArticlesHandler) + http.Handle("/", r) +} +``` + +Here we register three routes mapping URL paths to handlers. This is equivalent to how `http.HandleFunc()` works: if an incoming request URL matches one of the paths, the corresponding handler is called passing (`http.ResponseWriter`, `*http.Request`) as parameters. + +Paths can have variables. They are defined using the format `{name}` or `{name:pattern}`. If a regular expression pattern is not defined, the matched variable will be anything until the next slash. For example: + +```go +r := mux.NewRouter() +r.HandleFunc("/products/{key}", ProductHandler) +r.HandleFunc("/articles/{category}/", ArticlesCategoryHandler) +r.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler) +``` + +The names are used to create a map of route variables which can be retrieved calling `mux.Vars()`: + +```go +func ArticlesCategoryHandler(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, "Category: %v\n", vars["category"]) +} +``` + +And this is all you need to know about the basic usage. More advanced options are explained below. + +### Matching Routes + +Routes can also be restricted to a domain or subdomain. Just define a host pattern to be matched. They can also have variables: + +```go +r := mux.NewRouter() +// Only matches if domain is "www.example.com". +r.Host("www.example.com") +// Matches a dynamic subdomain. +r.Host("{subdomain:[a-z]+}.example.com") +``` + +There are several other matchers that can be added. To match path prefixes: + +```go +r.PathPrefix("/products/") +``` + +...or HTTP methods: + +```go +r.Methods("GET", "POST") +``` + +...or URL schemes: + +```go +r.Schemes("https") +``` + +...or header values: + +```go +r.Headers("X-Requested-With", "XMLHttpRequest") +``` + +...or query values: + +```go +r.Queries("key", "value") +``` + +...or to use a custom matcher function: + +```go +r.MatcherFunc(func(r *http.Request, rm *RouteMatch) bool { + return r.ProtoMajor == 0 +}) +``` + +...and finally, it is possible to combine several matchers in a single route: + +```go +r.HandleFunc("/products", ProductsHandler). + Host("www.example.com"). + Methods("GET"). + Schemes("http") +``` + +Routes are tested in the order they were added to the router. If two routes match, the first one wins: + +```go +r := mux.NewRouter() +r.HandleFunc("/specific", specificHandler) +r.PathPrefix("/").Handler(catchAllHandler) +``` + +Setting the same matching conditions again and again can be boring, so we have a way to group several routes that share the same requirements. We call it "subrouting". + +For example, let's say we have several URLs that should only match when the host is `www.example.com`. Create a route for that host and get a "subrouter" from it: + +```go +r := mux.NewRouter() +s := r.Host("www.example.com").Subrouter() +``` + +Then register routes in the subrouter: + +```go +s.HandleFunc("/products/", ProductsHandler) +s.HandleFunc("/products/{key}", ProductHandler) +s.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler) +``` + +The three URL paths we registered above will only be tested if the domain is `www.example.com`, because the subrouter is tested first. This is not only convenient, but also optimizes request matching. You can create subrouters combining any attribute matchers accepted by a route. + +Subrouters can be used to create domain or path "namespaces": you define subrouters in a central place and then parts of the app can register its paths relatively to a given subrouter. + +There's one more thing about subroutes. When a subrouter has a path prefix, the inner routes use it as base for their paths: + +```go +r := mux.NewRouter() +s := r.PathPrefix("/products").Subrouter() +// "/products/" +s.HandleFunc("/", ProductsHandler) +// "/products/{key}/" +s.HandleFunc("/{key}/", ProductHandler) +// "/products/{key}/details" +s.HandleFunc("/{key}/details", ProductDetailsHandler) +``` + + +### Static Files + +Note that the path provided to `PathPrefix()` represents a "wildcard": calling +`PathPrefix("/static/").Handler(...)` means that the handler will be passed any +request that matches "/static/\*". This makes it easy to serve static files with mux: + +```go +func main() { + var dir string + + flag.StringVar(&dir, "dir", ".", "the directory to serve files from. Defaults to the current dir") + flag.Parse() + r := mux.NewRouter() + + // This will serve files under http://localhost:8000/static/ + r.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.Dir(dir)))) + + srv := &http.Server{ + Handler: r, + Addr: "127.0.0.1:8000", + // Good practice: enforce timeouts for servers you create! + WriteTimeout: 15 * time.Second, + ReadTimeout: 15 * time.Second, + } + + log.Fatal(srv.ListenAndServe()) +} +``` + +### Serving Single Page Applications + +Most of the time it makes sense to serve your SPA on a separate web server from your API, +but sometimes it's desirable to serve them both from one place. It's possible to write a simple +handler for serving your SPA (for use with React Router's [BrowserRouter](https://reacttraining.com/react-router/web/api/BrowserRouter) for example), and leverage +mux's powerful routing for your API endpoints. + +```go +package main + +import ( + "encoding/json" + "log" + "net/http" + "os" + "path/filepath" + "time" + + "github.com/gorilla/mux" +) + +// spaHandler implements the http.Handler interface, so we can use it +// to respond to HTTP requests. The path to the static directory and +// path to the index file within that static directory are used to +// serve the SPA in the given static directory. +type spaHandler struct { + staticPath string + indexPath string +} + +// ServeHTTP inspects the URL path to locate a file within the static dir +// on the SPA handler. If a file is found, it will be served. If not, the +// file located at the index path on the SPA handler will be served. This +// is suitable behavior for serving an SPA (single page application). +func (h spaHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + // Join internally call path.Clean to prevent directory traversal + path := filepath.Join(h.staticPath, r.URL.Path) + + // check whether a file exists or is a directory at the given path + fi, err := os.Stat(path) + if os.IsNotExist(err) || fi.IsDir() { + // file does not exist or path is a directory, serve index.html + http.ServeFile(w, r, filepath.Join(h.staticPath, h.indexPath)) + return + } + + if err != nil { + // if we got an error (that wasn't that the file doesn't exist) stating the + // file, return a 500 internal server error and stop + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + // otherwise, use http.FileServer to serve the static file + http.FileServer(http.Dir(h.staticPath)).ServeHTTP(w, r) +} + +func main() { + router := mux.NewRouter() + + router.HandleFunc("/api/health", func(w http.ResponseWriter, r *http.Request) { + // an example API handler + json.NewEncoder(w).Encode(map[string]bool{"ok": true}) + }) + + spa := spaHandler{staticPath: "build", indexPath: "index.html"} + router.PathPrefix("/").Handler(spa) + + srv := &http.Server{ + Handler: router, + Addr: "127.0.0.1:8000", + // Good practice: enforce timeouts for servers you create! + WriteTimeout: 15 * time.Second, + ReadTimeout: 15 * time.Second, + } + + log.Fatal(srv.ListenAndServe()) +} +``` + +### Registered URLs + +Now let's see how to build registered URLs. + +Routes can be named. All routes that define a name can have their URLs built, or "reversed". We define a name calling `Name()` on a route. For example: + +```go +r := mux.NewRouter() +r.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler). + Name("article") +``` + +To build a URL, get the route and call the `URL()` method, passing a sequence of key/value pairs for the route variables. For the previous route, we would do: + +```go +url, err := r.Get("article").URL("category", "technology", "id", "42") +``` + +...and the result will be a `url.URL` with the following path: + +``` +"/articles/technology/42" +``` + +This also works for host and query value variables: + +```go +r := mux.NewRouter() +r.Host("{subdomain}.example.com"). + Path("/articles/{category}/{id:[0-9]+}"). + Queries("filter", "{filter}"). + HandlerFunc(ArticleHandler). + Name("article") + +// url.String() will be "http://news.example.com/articles/technology/42?filter=gorilla" +url, err := r.Get("article").URL("subdomain", "news", + "category", "technology", + "id", "42", + "filter", "gorilla") +``` + +All variables defined in the route are required, and their values must conform to the corresponding patterns. These requirements guarantee that a generated URL will always match a registered route -- the only exception is for explicitly defined "build-only" routes which never match. + +Regex support also exists for matching Headers within a route. For example, we could do: + +```go +r.HeadersRegexp("Content-Type", "application/(text|json)") +``` + +...and the route will match both requests with a Content-Type of `application/json` as well as `application/text` + +There's also a way to build only the URL host or path for a route: use the methods `URLHost()` or `URLPath()` instead. For the previous route, we would do: + +```go +// "http://news.example.com/" +host, err := r.Get("article").URLHost("subdomain", "news") + +// "/articles/technology/42" +path, err := r.Get("article").URLPath("category", "technology", "id", "42") +``` + +And if you use subrouters, host and path defined separately can be built as well: + +```go +r := mux.NewRouter() +s := r.Host("{subdomain}.example.com").Subrouter() +s.Path("/articles/{category}/{id:[0-9]+}"). + HandlerFunc(ArticleHandler). + Name("article") + +// "http://news.example.com/articles/technology/42" +url, err := r.Get("article").URL("subdomain", "news", + "category", "technology", + "id", "42") +``` + +To find all the required variables for a given route when calling `URL()`, the method `GetVarNames()` is available: +```go +r := mux.NewRouter() +r.Host("{domain}"). + Path("/{group}/{item_id}"). + Queries("some_data1", "{some_data1}"). + Queries("some_data2", "{some_data2}"). + Name("article") + +// Will print [domain group item_id some_data1 some_data2] +fmt.Println(r.Get("article").GetVarNames()) + +``` +### Walking Routes + +The `Walk` function on `mux.Router` can be used to visit all of the routes that are registered on a router. For example, +the following prints all of the registered routes: + +```go +package main + +import ( + "fmt" + "net/http" + "strings" + + "github.com/gorilla/mux" +) + +func handler(w http.ResponseWriter, r *http.Request) { + return +} + +func main() { + r := mux.NewRouter() + r.HandleFunc("/", handler) + r.HandleFunc("/products", handler).Methods("POST") + r.HandleFunc("/articles", handler).Methods("GET") + r.HandleFunc("/articles/{id}", handler).Methods("GET", "PUT") + r.HandleFunc("/authors", handler).Queries("surname", "{surname}") + err := r.Walk(func(route *mux.Route, router *mux.Router, ancestors []*mux.Route) error { + pathTemplate, err := route.GetPathTemplate() + if err == nil { + fmt.Println("ROUTE:", pathTemplate) + } + pathRegexp, err := route.GetPathRegexp() + if err == nil { + fmt.Println("Path regexp:", pathRegexp) + } + queriesTemplates, err := route.GetQueriesTemplates() + if err == nil { + fmt.Println("Queries templates:", strings.Join(queriesTemplates, ",")) + } + queriesRegexps, err := route.GetQueriesRegexp() + if err == nil { + fmt.Println("Queries regexps:", strings.Join(queriesRegexps, ",")) + } + methods, err := route.GetMethods() + if err == nil { + fmt.Println("Methods:", strings.Join(methods, ",")) + } + fmt.Println() + return nil + }) + + if err != nil { + fmt.Println(err) + } + + http.Handle("/", r) +} +``` + +### Graceful Shutdown + +Go 1.8 introduced the ability to [gracefully shutdown](https://golang.org/doc/go1.8#http_shutdown) a `*http.Server`. Here's how to do that alongside `mux`: + +```go +package main + +import ( + "context" + "flag" + "log" + "net/http" + "os" + "os/signal" + "time" + + "github.com/gorilla/mux" +) + +func main() { + var wait time.Duration + flag.DurationVar(&wait, "graceful-timeout", time.Second * 15, "the duration for which the server gracefully wait for existing connections to finish - e.g. 15s or 1m") + flag.Parse() + + r := mux.NewRouter() + // Add your routes as needed + + srv := &http.Server{ + Addr: "0.0.0.0:8080", + // Good practice to set timeouts to avoid Slowloris attacks. + WriteTimeout: time.Second * 15, + ReadTimeout: time.Second * 15, + IdleTimeout: time.Second * 60, + Handler: r, // Pass our instance of gorilla/mux in. + } + + // Run our server in a goroutine so that it doesn't block. + go func() { + if err := srv.ListenAndServe(); err != nil { + log.Println(err) + } + }() + + c := make(chan os.Signal, 1) + // We'll accept graceful shutdowns when quit via SIGINT (Ctrl+C) + // SIGKILL, SIGQUIT or SIGTERM (Ctrl+/) will not be caught. + signal.Notify(c, os.Interrupt) + + // Block until we receive our signal. + <-c + + // Create a deadline to wait for. + ctx, cancel := context.WithTimeout(context.Background(), wait) + defer cancel() + // Doesn't block if no connections, but will otherwise wait + // until the timeout deadline. + srv.Shutdown(ctx) + // Optionally, you could run srv.Shutdown in a goroutine and block on + // <-ctx.Done() if your application should wait for other services + // to finalize based on context cancellation. + log.Println("shutting down") + os.Exit(0) +} +``` + +### Middleware + +Mux supports the addition of middlewares to a [Router](https://godoc.org/github.com/gorilla/mux#Router), which are executed in the order they are added if a match is found, including its subrouters. +Middlewares are (typically) small pieces of code which take one request, do something with it, and pass it down to another middleware or the final handler. Some common use cases for middleware are request logging, header manipulation, or `ResponseWriter` hijacking. + +Mux middlewares are defined using the de facto standard type: + +```go +type MiddlewareFunc func(http.Handler) http.Handler +``` + +Typically, the returned handler is a closure which does something with the http.ResponseWriter and http.Request passed to it, and then calls the handler passed as parameter to the MiddlewareFunc. This takes advantage of closures being able access variables from the context where they are created, while retaining the signature enforced by the receivers. + +A very basic middleware which logs the URI of the request being handled could be written as: + +```go +func loggingMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Do stuff here + log.Println(r.RequestURI) + // Call the next handler, which can be another middleware in the chain, or the final handler. + next.ServeHTTP(w, r) + }) +} +``` + +Middlewares can be added to a router using `Router.Use()`: + +```go +r := mux.NewRouter() +r.HandleFunc("/", handler) +r.Use(loggingMiddleware) +``` + +A more complex authentication middleware, which maps session token to users, could be written as: + +```go +// Define our struct +type authenticationMiddleware struct { + tokenUsers map[string]string +} + +// Initialize it somewhere +func (amw *authenticationMiddleware) Populate() { + amw.tokenUsers["00000000"] = "user0" + amw.tokenUsers["aaaaaaaa"] = "userA" + amw.tokenUsers["05f717e5"] = "randomUser" + amw.tokenUsers["deadbeef"] = "user0" +} + +// Middleware function, which will be called for each request +func (amw *authenticationMiddleware) Middleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + token := r.Header.Get("X-Session-Token") + + if user, found := amw.tokenUsers[token]; found { + // We found the token in our map + log.Printf("Authenticated user %s\n", user) + // Pass down the request to the next middleware (or final handler) + next.ServeHTTP(w, r) + } else { + // Write an error and stop the handler chain + http.Error(w, "Forbidden", http.StatusForbidden) + } + }) +} +``` + +```go +r := mux.NewRouter() +r.HandleFunc("/", handler) + +amw := authenticationMiddleware{tokenUsers: make(map[string]string)} +amw.Populate() + +r.Use(amw.Middleware) +``` + +Note: The handler chain will be stopped if your middleware doesn't call `next.ServeHTTP()` with the corresponding parameters. This can be used to abort a request if the middleware writer wants to. Middlewares _should_ write to `ResponseWriter` if they _are_ going to terminate the request, and they _should not_ write to `ResponseWriter` if they _are not_ going to terminate it. + +### Handling CORS Requests + +[CORSMethodMiddleware](https://godoc.org/github.com/gorilla/mux#CORSMethodMiddleware) intends to make it easier to strictly set the `Access-Control-Allow-Methods` response header. + +* You will still need to use your own CORS handler to set the other CORS headers such as `Access-Control-Allow-Origin` +* The middleware will set the `Access-Control-Allow-Methods` header to all the method matchers (e.g. `r.Methods(http.MethodGet, http.MethodPut, http.MethodOptions)` -> `Access-Control-Allow-Methods: GET,PUT,OPTIONS`) on a route +* If you do not specify any methods, then: +> _Important_: there must be an `OPTIONS` method matcher for the middleware to set the headers. + +Here is an example of using `CORSMethodMiddleware` along with a custom `OPTIONS` handler to set all the required CORS headers: + +```go +package main + +import ( + "net/http" + "github.com/gorilla/mux" +) + +func main() { + r := mux.NewRouter() + + // IMPORTANT: you must specify an OPTIONS method matcher for the middleware to set CORS headers + r.HandleFunc("/foo", fooHandler).Methods(http.MethodGet, http.MethodPut, http.MethodPatch, http.MethodOptions) + r.Use(mux.CORSMethodMiddleware(r)) + + http.ListenAndServe(":8080", r) +} + +func fooHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Access-Control-Allow-Origin", "*") + if r.Method == http.MethodOptions { + return + } + + w.Write([]byte("foo")) +} +``` + +And an request to `/foo` using something like: + +```bash +curl localhost:8080/foo -v +``` + +Would look like: + +```bash +* Trying ::1... +* TCP_NODELAY set +* Connected to localhost (::1) port 8080 (#0) +> GET /foo HTTP/1.1 +> Host: localhost:8080 +> User-Agent: curl/7.59.0 +> Accept: */* +> +< HTTP/1.1 200 OK +< Access-Control-Allow-Methods: GET,PUT,PATCH,OPTIONS +< Access-Control-Allow-Origin: * +< Date: Fri, 28 Jun 2019 20:13:30 GMT +< Content-Length: 3 +< Content-Type: text/plain; charset=utf-8 +< +* Connection #0 to host localhost left intact +foo +``` + +### Testing Handlers + +Testing handlers in a Go web application is straightforward, and _mux_ doesn't complicate this any further. Given two files: `endpoints.go` and `endpoints_test.go`, here's how we'd test an application using _mux_. + +First, our simple HTTP handler: + +```go +// endpoints.go +package main + +func HealthCheckHandler(w http.ResponseWriter, r *http.Request) { + // A very simple health check. + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + // In the future we could report back on the status of our DB, or our cache + // (e.g. Redis) by performing a simple PING, and include them in the response. + io.WriteString(w, `{"alive": true}`) +} + +func main() { + r := mux.NewRouter() + r.HandleFunc("/health", HealthCheckHandler) + + log.Fatal(http.ListenAndServe("localhost:8080", r)) +} +``` + +Our test code: + +```go +// endpoints_test.go +package main + +import ( + "net/http" + "net/http/httptest" + "testing" +) + +func TestHealthCheckHandler(t *testing.T) { + // Create a request to pass to our handler. We don't have any query parameters for now, so we'll + // pass 'nil' as the third parameter. + req, err := http.NewRequest("GET", "/health", nil) + if err != nil { + t.Fatal(err) + } + + // We create a ResponseRecorder (which satisfies http.ResponseWriter) to record the response. + rr := httptest.NewRecorder() + handler := http.HandlerFunc(HealthCheckHandler) + + // Our handlers satisfy http.Handler, so we can call their ServeHTTP method + // directly and pass in our Request and ResponseRecorder. + handler.ServeHTTP(rr, req) + + // Check the status code is what we expect. + if status := rr.Code; status != http.StatusOK { + t.Errorf("handler returned wrong status code: got %v want %v", + status, http.StatusOK) + } + + // Check the response body is what we expect. + expected := `{"alive": true}` + if rr.Body.String() != expected { + t.Errorf("handler returned unexpected body: got %v want %v", + rr.Body.String(), expected) + } +} +``` + +In the case that our routes have [variables](#examples), we can pass those in the request. We could write +[table-driven tests](https://dave.cheney.net/2013/06/09/writing-table-driven-tests-in-go) to test multiple +possible route variables as needed. + +```go +// endpoints.go +func main() { + r := mux.NewRouter() + // A route with a route variable: + r.HandleFunc("/metrics/{type}", MetricsHandler) + + log.Fatal(http.ListenAndServe("localhost:8080", r)) +} +``` + +Our test file, with a table-driven test of `routeVariables`: + +```go +// endpoints_test.go +func TestMetricsHandler(t *testing.T) { + tt := []struct{ + routeVariable string + shouldPass bool + }{ + {"goroutines", true}, + {"heap", true}, + {"counters", true}, + {"queries", true}, + {"adhadaeqm3k", false}, + } + + for _, tc := range tt { + path := fmt.Sprintf("/metrics/%s", tc.routeVariable) + req, err := http.NewRequest("GET", path, nil) + if err != nil { + t.Fatal(err) + } + + rr := httptest.NewRecorder() + + // To add the vars to the context, + // we need to create a router through which we can pass the request. + router := mux.NewRouter() + router.HandleFunc("/metrics/{type}", MetricsHandler) + router.ServeHTTP(rr, req) + + // In this case, our MetricsHandler returns a non-200 response + // for a route variable it doesn't know about. + if rr.Code == http.StatusOK && !tc.shouldPass { + t.Errorf("handler should have failed on routeVariable %s: got %v want %v", + tc.routeVariable, rr.Code, http.StatusOK) + } + } +} +``` + +## Full Example + +Here's a complete, runnable example of a small `mux` based server: + +```go +package main + +import ( + "net/http" + "log" + "github.com/gorilla/mux" +) + +func YourHandler(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("Gorilla!\n")) +} + +func main() { + r := mux.NewRouter() + // Routes consist of a path and a handler function. + r.HandleFunc("/", YourHandler) + + // Bind to a port and pass our router in + log.Fatal(http.ListenAndServe(":8000", r)) +} +``` + +## License + +BSD licensed. See the LICENSE file for details. diff --git a/vendor/github.com/gorilla/mux/doc.go b/vendor/github.com/gorilla/mux/doc.go new file mode 100644 index 00000000..80601351 --- /dev/null +++ b/vendor/github.com/gorilla/mux/doc.go @@ -0,0 +1,305 @@ +// Copyright 2012 The Gorilla 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 mux implements a request router and dispatcher. + +The name mux stands for "HTTP request multiplexer". Like the standard +http.ServeMux, mux.Router matches incoming requests against a list of +registered routes and calls a handler for the route that matches the URL +or other conditions. The main features are: + + - Requests can be matched based on URL host, path, path prefix, schemes, + header and query values, HTTP methods or using custom matchers. + - URL hosts, paths and query values can have variables with an optional + regular expression. + - Registered URLs can be built, or "reversed", which helps maintaining + references to resources. + - Routes can be used as subrouters: nested routes are only tested if the + parent route matches. This is useful to define groups of routes that + share common conditions like a host, a path prefix or other repeated + attributes. As a bonus, this optimizes request matching. + - It implements the http.Handler interface so it is compatible with the + standard http.ServeMux. + +Let's start registering a couple of URL paths and handlers: + + func main() { + r := mux.NewRouter() + r.HandleFunc("/", HomeHandler) + r.HandleFunc("/products", ProductsHandler) + r.HandleFunc("/articles", ArticlesHandler) + http.Handle("/", r) + } + +Here we register three routes mapping URL paths to handlers. This is +equivalent to how http.HandleFunc() works: if an incoming request URL matches +one of the paths, the corresponding handler is called passing +(http.ResponseWriter, *http.Request) as parameters. + +Paths can have variables. They are defined using the format {name} or +{name:pattern}. If a regular expression pattern is not defined, the matched +variable will be anything until the next slash. For example: + + r := mux.NewRouter() + r.HandleFunc("/products/{key}", ProductHandler) + r.HandleFunc("/articles/{category}/", ArticlesCategoryHandler) + r.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler) + +Groups can be used inside patterns, as long as they are non-capturing (?:re). For example: + + r.HandleFunc("/articles/{category}/{sort:(?:asc|desc|new)}", ArticlesCategoryHandler) + +The names are used to create a map of route variables which can be retrieved +calling mux.Vars(): + + vars := mux.Vars(request) + category := vars["category"] + +Note that if any capturing groups are present, mux will panic() during parsing. To prevent +this, convert any capturing groups to non-capturing, e.g. change "/{sort:(asc|desc)}" to +"/{sort:(?:asc|desc)}". This is a change from prior versions which behaved unpredictably +when capturing groups were present. + +And this is all you need to know about the basic usage. More advanced options +are explained below. + +Routes can also be restricted to a domain or subdomain. Just define a host +pattern to be matched. They can also have variables: + + r := mux.NewRouter() + // Only matches if domain is "www.example.com". + r.Host("www.example.com") + // Matches a dynamic subdomain. + r.Host("{subdomain:[a-z]+}.domain.com") + +There are several other matchers that can be added. To match path prefixes: + + r.PathPrefix("/products/") + +...or HTTP methods: + + r.Methods("GET", "POST") + +...or URL schemes: + + r.Schemes("https") + +...or header values: + + r.Headers("X-Requested-With", "XMLHttpRequest") + +...or query values: + + r.Queries("key", "value") + +...or to use a custom matcher function: + + r.MatcherFunc(func(r *http.Request, rm *RouteMatch) bool { + return r.ProtoMajor == 0 + }) + +...and finally, it is possible to combine several matchers in a single route: + + r.HandleFunc("/products", ProductsHandler). + Host("www.example.com"). + Methods("GET"). + Schemes("http") + +Setting the same matching conditions again and again can be boring, so we have +a way to group several routes that share the same requirements. +We call it "subrouting". + +For example, let's say we have several URLs that should only match when the +host is "www.example.com". Create a route for that host and get a "subrouter" +from it: + + r := mux.NewRouter() + s := r.Host("www.example.com").Subrouter() + +Then register routes in the subrouter: + + s.HandleFunc("/products/", ProductsHandler) + s.HandleFunc("/products/{key}", ProductHandler) + s.HandleFunc("/articles/{category}/{id:[0-9]+}"), ArticleHandler) + +The three URL paths we registered above will only be tested if the domain is +"www.example.com", because the subrouter is tested first. This is not +only convenient, but also optimizes request matching. You can create +subrouters combining any attribute matchers accepted by a route. + +Subrouters can be used to create domain or path "namespaces": you define +subrouters in a central place and then parts of the app can register its +paths relatively to a given subrouter. + +There's one more thing about subroutes. When a subrouter has a path prefix, +the inner routes use it as base for their paths: + + r := mux.NewRouter() + s := r.PathPrefix("/products").Subrouter() + // "/products/" + s.HandleFunc("/", ProductsHandler) + // "/products/{key}/" + s.HandleFunc("/{key}/", ProductHandler) + // "/products/{key}/details" + s.HandleFunc("/{key}/details", ProductDetailsHandler) + +Note that the path provided to PathPrefix() represents a "wildcard": calling +PathPrefix("/static/").Handler(...) means that the handler will be passed any +request that matches "/static/*". This makes it easy to serve static files with mux: + + func main() { + var dir string + + flag.StringVar(&dir, "dir", ".", "the directory to serve files from. Defaults to the current dir") + flag.Parse() + r := mux.NewRouter() + + // This will serve files under http://localhost:8000/static/ + r.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.Dir(dir)))) + + srv := &http.Server{ + Handler: r, + Addr: "127.0.0.1:8000", + // Good practice: enforce timeouts for servers you create! + WriteTimeout: 15 * time.Second, + ReadTimeout: 15 * time.Second, + } + + log.Fatal(srv.ListenAndServe()) + } + +Now let's see how to build registered URLs. + +Routes can be named. All routes that define a name can have their URLs built, +or "reversed". We define a name calling Name() on a route. For example: + + r := mux.NewRouter() + r.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler). + Name("article") + +To build a URL, get the route and call the URL() method, passing a sequence of +key/value pairs for the route variables. For the previous route, we would do: + + url, err := r.Get("article").URL("category", "technology", "id", "42") + +...and the result will be a url.URL with the following path: + + "/articles/technology/42" + +This also works for host and query value variables: + + r := mux.NewRouter() + r.Host("{subdomain}.domain.com"). + Path("/articles/{category}/{id:[0-9]+}"). + Queries("filter", "{filter}"). + HandlerFunc(ArticleHandler). + Name("article") + + // url.String() will be "http://news.domain.com/articles/technology/42?filter=gorilla" + url, err := r.Get("article").URL("subdomain", "news", + "category", "technology", + "id", "42", + "filter", "gorilla") + +All variables defined in the route are required, and their values must +conform to the corresponding patterns. These requirements guarantee that a +generated URL will always match a registered route -- the only exception is +for explicitly defined "build-only" routes which never match. + +Regex support also exists for matching Headers within a route. For example, we could do: + + r.HeadersRegexp("Content-Type", "application/(text|json)") + +...and the route will match both requests with a Content-Type of `application/json` as well as +`application/text` + +There's also a way to build only the URL host or path for a route: +use the methods URLHost() or URLPath() instead. For the previous route, +we would do: + + // "http://news.domain.com/" + host, err := r.Get("article").URLHost("subdomain", "news") + + // "/articles/technology/42" + path, err := r.Get("article").URLPath("category", "technology", "id", "42") + +And if you use subrouters, host and path defined separately can be built +as well: + + r := mux.NewRouter() + s := r.Host("{subdomain}.domain.com").Subrouter() + s.Path("/articles/{category}/{id:[0-9]+}"). + HandlerFunc(ArticleHandler). + Name("article") + + // "http://news.domain.com/articles/technology/42" + url, err := r.Get("article").URL("subdomain", "news", + "category", "technology", + "id", "42") + +Mux supports the addition of middlewares to a Router, which are executed in the order they are added if a match is found, including its subrouters. Middlewares are (typically) small pieces of code which take one request, do something with it, and pass it down to another middleware or the final handler. Some common use cases for middleware are request logging, header manipulation, or ResponseWriter hijacking. + + type MiddlewareFunc func(http.Handler) http.Handler + +Typically, the returned handler is a closure which does something with the http.ResponseWriter and http.Request passed to it, and then calls the handler passed as parameter to the MiddlewareFunc (closures can access variables from the context where they are created). + +A very basic middleware which logs the URI of the request being handled could be written as: + + func simpleMw(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Do stuff here + log.Println(r.RequestURI) + // Call the next handler, which can be another middleware in the chain, or the final handler. + next.ServeHTTP(w, r) + }) + } + +Middlewares can be added to a router using `Router.Use()`: + + r := mux.NewRouter() + r.HandleFunc("/", handler) + r.Use(simpleMw) + +A more complex authentication middleware, which maps session token to users, could be written as: + + // Define our struct + type authenticationMiddleware struct { + tokenUsers map[string]string + } + + // Initialize it somewhere + func (amw *authenticationMiddleware) Populate() { + amw.tokenUsers["00000000"] = "user0" + amw.tokenUsers["aaaaaaaa"] = "userA" + amw.tokenUsers["05f717e5"] = "randomUser" + amw.tokenUsers["deadbeef"] = "user0" + } + + // Middleware function, which will be called for each request + func (amw *authenticationMiddleware) Middleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + token := r.Header.Get("X-Session-Token") + + if user, found := amw.tokenUsers[token]; found { + // We found the token in our map + log.Printf("Authenticated user %s\n", user) + next.ServeHTTP(w, r) + } else { + http.Error(w, "Forbidden", http.StatusForbidden) + } + }) + } + + r := mux.NewRouter() + r.HandleFunc("/", handler) + + amw := authenticationMiddleware{tokenUsers: make(map[string]string)} + amw.Populate() + + r.Use(amw.Middleware) + +Note: The handler chain will be stopped if your middleware doesn't call `next.ServeHTTP()` with the corresponding parameters. This can be used to abort a request if the middleware writer wants to. +*/ +package mux diff --git a/vendor/github.com/gorilla/mux/middleware.go b/vendor/github.com/gorilla/mux/middleware.go new file mode 100644 index 00000000..cb51c565 --- /dev/null +++ b/vendor/github.com/gorilla/mux/middleware.go @@ -0,0 +1,74 @@ +package mux + +import ( + "net/http" + "strings" +) + +// MiddlewareFunc is a function which receives an http.Handler and returns another http.Handler. +// Typically, the returned handler is a closure which does something with the http.ResponseWriter and http.Request passed +// to it, and then calls the handler passed as parameter to the MiddlewareFunc. +type MiddlewareFunc func(http.Handler) http.Handler + +// middleware interface is anything which implements a MiddlewareFunc named Middleware. +type middleware interface { + Middleware(handler http.Handler) http.Handler +} + +// Middleware allows MiddlewareFunc to implement the middleware interface. +func (mw MiddlewareFunc) Middleware(handler http.Handler) http.Handler { + return mw(handler) +} + +// Use appends a MiddlewareFunc to the chain. Middleware can be used to intercept or otherwise modify requests and/or responses, and are executed in the order that they are applied to the Router. +func (r *Router) Use(mwf ...MiddlewareFunc) { + for _, fn := range mwf { + r.middlewares = append(r.middlewares, fn) + } +} + +// useInterface appends a middleware to the chain. Middleware can be used to intercept or otherwise modify requests and/or responses, and are executed in the order that they are applied to the Router. +func (r *Router) useInterface(mw middleware) { + r.middlewares = append(r.middlewares, mw) +} + +// CORSMethodMiddleware automatically sets the Access-Control-Allow-Methods response header +// on requests for routes that have an OPTIONS method matcher to all the method matchers on +// the route. Routes that do not explicitly handle OPTIONS requests will not be processed +// by the middleware. See examples for usage. +func CORSMethodMiddleware(r *Router) MiddlewareFunc { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + allMethods, err := getAllMethodsForRoute(r, req) + if err == nil { + for _, v := range allMethods { + if v == http.MethodOptions { + w.Header().Set("Access-Control-Allow-Methods", strings.Join(allMethods, ",")) + } + } + } + + next.ServeHTTP(w, req) + }) + } +} + +// getAllMethodsForRoute returns all the methods from method matchers matching a given +// request. +func getAllMethodsForRoute(r *Router, req *http.Request) ([]string, error) { + var allMethods []string + + for _, route := range r.routes { + var match RouteMatch + if route.Match(req, &match) || match.MatchErr == ErrMethodMismatch { + methods, err := route.GetMethods() + if err != nil { + return nil, err + } + + allMethods = append(allMethods, methods...) + } + } + + return allMethods, nil +} diff --git a/vendor/github.com/gorilla/mux/mux.go b/vendor/github.com/gorilla/mux/mux.go new file mode 100644 index 00000000..1e089906 --- /dev/null +++ b/vendor/github.com/gorilla/mux/mux.go @@ -0,0 +1,608 @@ +// Copyright 2012 The Gorilla 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 mux + +import ( + "context" + "errors" + "fmt" + "net/http" + "path" + "regexp" +) + +var ( + // ErrMethodMismatch is returned when the method in the request does not match + // the method defined against the route. + ErrMethodMismatch = errors.New("method is not allowed") + // ErrNotFound is returned when no route match is found. + ErrNotFound = errors.New("no matching route was found") +) + +// NewRouter returns a new router instance. +func NewRouter() *Router { + return &Router{namedRoutes: make(map[string]*Route)} +} + +// Router registers routes to be matched and dispatches a handler. +// +// It implements the http.Handler interface, so it can be registered to serve +// requests: +// +// var router = mux.NewRouter() +// +// func main() { +// http.Handle("/", router) +// } +// +// Or, for Google App Engine, register it in a init() function: +// +// func init() { +// http.Handle("/", router) +// } +// +// This will send all incoming requests to the router. +type Router struct { + // Configurable Handler to be used when no route matches. + // This can be used to render your own 404 Not Found errors. + NotFoundHandler http.Handler + + // Configurable Handler to be used when the request method does not match the route. + // This can be used to render your own 405 Method Not Allowed errors. + MethodNotAllowedHandler http.Handler + + // Routes to be matched, in order. + routes []*Route + + // Routes by name for URL building. + namedRoutes map[string]*Route + + // If true, do not clear the request context after handling the request. + // + // Deprecated: No effect, since the context is stored on the request itself. + KeepContext bool + + // Slice of middlewares to be called after a match is found + middlewares []middleware + + // configuration shared with `Route` + routeConf +} + +// common route configuration shared between `Router` and `Route` +type routeConf struct { + // If true, "/path/foo%2Fbar/to" will match the path "/path/{var}/to" + useEncodedPath bool + + // If true, when the path pattern is "/path/", accessing "/path" will + // redirect to the former and vice versa. + strictSlash bool + + // If true, when the path pattern is "/path//to", accessing "/path//to" + // will not redirect + skipClean bool + + // Manager for the variables from host and path. + regexp routeRegexpGroup + + // List of matchers. + matchers []matcher + + // The scheme used when building URLs. + buildScheme string + + buildVarsFunc BuildVarsFunc +} + +// returns an effective deep copy of `routeConf` +func copyRouteConf(r routeConf) routeConf { + c := r + + if r.regexp.path != nil { + c.regexp.path = copyRouteRegexp(r.regexp.path) + } + + if r.regexp.host != nil { + c.regexp.host = copyRouteRegexp(r.regexp.host) + } + + c.regexp.queries = make([]*routeRegexp, 0, len(r.regexp.queries)) + for _, q := range r.regexp.queries { + c.regexp.queries = append(c.regexp.queries, copyRouteRegexp(q)) + } + + c.matchers = make([]matcher, len(r.matchers)) + copy(c.matchers, r.matchers) + + return c +} + +func copyRouteRegexp(r *routeRegexp) *routeRegexp { + c := *r + return &c +} + +// Match attempts to match the given request against the router's registered routes. +// +// If the request matches a route of this router or one of its subrouters the Route, +// Handler, and Vars fields of the the match argument are filled and this function +// returns true. +// +// If the request does not match any of this router's or its subrouters' routes +// then this function returns false. If available, a reason for the match failure +// will be filled in the match argument's MatchErr field. If the match failure type +// (eg: not found) has a registered handler, the handler is assigned to the Handler +// field of the match argument. +func (r *Router) Match(req *http.Request, match *RouteMatch) bool { + for _, route := range r.routes { + if route.Match(req, match) { + // Build middleware chain if no error was found + if match.MatchErr == nil { + for i := len(r.middlewares) - 1; i >= 0; i-- { + match.Handler = r.middlewares[i].Middleware(match.Handler) + } + } + return true + } + } + + if match.MatchErr == ErrMethodMismatch { + if r.MethodNotAllowedHandler != nil { + match.Handler = r.MethodNotAllowedHandler + return true + } + + return false + } + + // Closest match for a router (includes sub-routers) + if r.NotFoundHandler != nil { + match.Handler = r.NotFoundHandler + match.MatchErr = ErrNotFound + return true + } + + match.MatchErr = ErrNotFound + return false +} + +// ServeHTTP dispatches the handler registered in the matched route. +// +// When there is a match, the route variables can be retrieved calling +// mux.Vars(request). +func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) { + if !r.skipClean { + path := req.URL.Path + if r.useEncodedPath { + path = req.URL.EscapedPath() + } + // Clean path to canonical form and redirect. + if p := cleanPath(path); p != path { + + // Added 3 lines (Philip Schlump) - It was dropping the query string and #whatever from query. + // This matches with fix in go 1.2 r.c. 4 for same problem. Go Issue: + // http://code.google.com/p/go/issues/detail?id=5252 + url := *req.URL + url.Path = p + p = url.String() + + w.Header().Set("Location", p) + w.WriteHeader(http.StatusMovedPermanently) + return + } + } + var match RouteMatch + var handler http.Handler + if r.Match(req, &match) { + handler = match.Handler + req = requestWithVars(req, match.Vars) + req = requestWithRoute(req, match.Route) + } + + if handler == nil && match.MatchErr == ErrMethodMismatch { + handler = methodNotAllowedHandler() + } + + if handler == nil { + handler = http.NotFoundHandler() + } + + handler.ServeHTTP(w, req) +} + +// Get returns a route registered with the given name. +func (r *Router) Get(name string) *Route { + return r.namedRoutes[name] +} + +// GetRoute returns a route registered with the given name. This method +// was renamed to Get() and remains here for backwards compatibility. +func (r *Router) GetRoute(name string) *Route { + return r.namedRoutes[name] +} + +// StrictSlash defines the trailing slash behavior for new routes. The initial +// value is false. +// +// When true, if the route path is "/path/", accessing "/path" will perform a redirect +// to the former and vice versa. In other words, your application will always +// see the path as specified in the route. +// +// When false, if the route path is "/path", accessing "/path/" will not match +// this route and vice versa. +// +// The re-direct is a HTTP 301 (Moved Permanently). Note that when this is set for +// routes with a non-idempotent method (e.g. POST, PUT), the subsequent re-directed +// request will be made as a GET by most clients. Use middleware or client settings +// to modify this behaviour as needed. +// +// Special case: when a route sets a path prefix using the PathPrefix() method, +// strict slash is ignored for that route because the redirect behavior can't +// be determined from a prefix alone. However, any subrouters created from that +// route inherit the original StrictSlash setting. +func (r *Router) StrictSlash(value bool) *Router { + r.strictSlash = value + return r +} + +// SkipClean defines the path cleaning behaviour for new routes. The initial +// value is false. Users should be careful about which routes are not cleaned +// +// When true, if the route path is "/path//to", it will remain with the double +// slash. This is helpful if you have a route like: /fetch/http://xkcd.com/534/ +// +// When false, the path will be cleaned, so /fetch/http://xkcd.com/534/ will +// become /fetch/http/xkcd.com/534 +func (r *Router) SkipClean(value bool) *Router { + r.skipClean = value + return r +} + +// UseEncodedPath tells the router to match the encoded original path +// to the routes. +// For eg. "/path/foo%2Fbar/to" will match the path "/path/{var}/to". +// +// If not called, the router will match the unencoded path to the routes. +// For eg. "/path/foo%2Fbar/to" will match the path "/path/foo/bar/to" +func (r *Router) UseEncodedPath() *Router { + r.useEncodedPath = true + return r +} + +// ---------------------------------------------------------------------------- +// Route factories +// ---------------------------------------------------------------------------- + +// NewRoute registers an empty route. +func (r *Router) NewRoute() *Route { + // initialize a route with a copy of the parent router's configuration + route := &Route{routeConf: copyRouteConf(r.routeConf), namedRoutes: r.namedRoutes} + r.routes = append(r.routes, route) + return route +} + +// Name registers a new route with a name. +// See Route.Name(). +func (r *Router) Name(name string) *Route { + return r.NewRoute().Name(name) +} + +// Handle registers a new route with a matcher for the URL path. +// See Route.Path() and Route.Handler(). +func (r *Router) Handle(path string, handler http.Handler) *Route { + return r.NewRoute().Path(path).Handler(handler) +} + +// HandleFunc registers a new route with a matcher for the URL path. +// See Route.Path() and Route.HandlerFunc(). +func (r *Router) HandleFunc(path string, f func(http.ResponseWriter, + *http.Request)) *Route { + return r.NewRoute().Path(path).HandlerFunc(f) +} + +// Headers registers a new route with a matcher for request header values. +// See Route.Headers(). +func (r *Router) Headers(pairs ...string) *Route { + return r.NewRoute().Headers(pairs...) +} + +// Host registers a new route with a matcher for the URL host. +// See Route.Host(). +func (r *Router) Host(tpl string) *Route { + return r.NewRoute().Host(tpl) +} + +// MatcherFunc registers a new route with a custom matcher function. +// See Route.MatcherFunc(). +func (r *Router) MatcherFunc(f MatcherFunc) *Route { + return r.NewRoute().MatcherFunc(f) +} + +// Methods registers a new route with a matcher for HTTP methods. +// See Route.Methods(). +func (r *Router) Methods(methods ...string) *Route { + return r.NewRoute().Methods(methods...) +} + +// Path registers a new route with a matcher for the URL path. +// See Route.Path(). +func (r *Router) Path(tpl string) *Route { + return r.NewRoute().Path(tpl) +} + +// PathPrefix registers a new route with a matcher for the URL path prefix. +// See Route.PathPrefix(). +func (r *Router) PathPrefix(tpl string) *Route { + return r.NewRoute().PathPrefix(tpl) +} + +// Queries registers a new route with a matcher for URL query values. +// See Route.Queries(). +func (r *Router) Queries(pairs ...string) *Route { + return r.NewRoute().Queries(pairs...) +} + +// Schemes registers a new route with a matcher for URL schemes. +// See Route.Schemes(). +func (r *Router) Schemes(schemes ...string) *Route { + return r.NewRoute().Schemes(schemes...) +} + +// BuildVarsFunc registers a new route with a custom function for modifying +// route variables before building a URL. +func (r *Router) BuildVarsFunc(f BuildVarsFunc) *Route { + return r.NewRoute().BuildVarsFunc(f) +} + +// Walk walks the router and all its sub-routers, calling walkFn for each route +// in the tree. The routes are walked in the order they were added. Sub-routers +// are explored depth-first. +func (r *Router) Walk(walkFn WalkFunc) error { + return r.walk(walkFn, []*Route{}) +} + +// SkipRouter is used as a return value from WalkFuncs to indicate that the +// router that walk is about to descend down to should be skipped. +var SkipRouter = errors.New("skip this router") + +// WalkFunc is the type of the function called for each route visited by Walk. +// At every invocation, it is given the current route, and the current router, +// and a list of ancestor routes that lead to the current route. +type WalkFunc func(route *Route, router *Router, ancestors []*Route) error + +func (r *Router) walk(walkFn WalkFunc, ancestors []*Route) error { + for _, t := range r.routes { + err := walkFn(t, r, ancestors) + if err == SkipRouter { + continue + } + if err != nil { + return err + } + for _, sr := range t.matchers { + if h, ok := sr.(*Router); ok { + ancestors = append(ancestors, t) + err := h.walk(walkFn, ancestors) + if err != nil { + return err + } + ancestors = ancestors[:len(ancestors)-1] + } + } + if h, ok := t.handler.(*Router); ok { + ancestors = append(ancestors, t) + err := h.walk(walkFn, ancestors) + if err != nil { + return err + } + ancestors = ancestors[:len(ancestors)-1] + } + } + return nil +} + +// ---------------------------------------------------------------------------- +// Context +// ---------------------------------------------------------------------------- + +// RouteMatch stores information about a matched route. +type RouteMatch struct { + Route *Route + Handler http.Handler + Vars map[string]string + + // MatchErr is set to appropriate matching error + // It is set to ErrMethodMismatch if there is a mismatch in + // the request method and route method + MatchErr error +} + +type contextKey int + +const ( + varsKey contextKey = iota + routeKey +) + +// Vars returns the route variables for the current request, if any. +func Vars(r *http.Request) map[string]string { + if rv := r.Context().Value(varsKey); rv != nil { + return rv.(map[string]string) + } + return nil +} + +// CurrentRoute returns the matched route for the current request, if any. +// This only works when called inside the handler of the matched route +// because the matched route is stored in the request context which is cleared +// after the handler returns. +func CurrentRoute(r *http.Request) *Route { + if rv := r.Context().Value(routeKey); rv != nil { + return rv.(*Route) + } + return nil +} + +func requestWithVars(r *http.Request, vars map[string]string) *http.Request { + ctx := context.WithValue(r.Context(), varsKey, vars) + return r.WithContext(ctx) +} + +func requestWithRoute(r *http.Request, route *Route) *http.Request { + ctx := context.WithValue(r.Context(), routeKey, route) + return r.WithContext(ctx) +} + +// ---------------------------------------------------------------------------- +// Helpers +// ---------------------------------------------------------------------------- + +// cleanPath returns the canonical path for p, eliminating . and .. elements. +// Borrowed from the net/http package. +func cleanPath(p string) string { + if p == "" { + return "/" + } + if p[0] != '/' { + p = "/" + p + } + np := path.Clean(p) + // path.Clean removes trailing slash except for root; + // put the trailing slash back if necessary. + if p[len(p)-1] == '/' && np != "/" { + np += "/" + } + + return np +} + +// uniqueVars returns an error if two slices contain duplicated strings. +func uniqueVars(s1, s2 []string) error { + for _, v1 := range s1 { + for _, v2 := range s2 { + if v1 == v2 { + return fmt.Errorf("mux: duplicated route variable %q", v2) + } + } + } + return nil +} + +// checkPairs returns the count of strings passed in, and an error if +// the count is not an even number. +func checkPairs(pairs ...string) (int, error) { + length := len(pairs) + if length%2 != 0 { + return length, fmt.Errorf( + "mux: number of parameters must be multiple of 2, got %v", pairs) + } + return length, nil +} + +// mapFromPairsToString converts variadic string parameters to a +// string to string map. +func mapFromPairsToString(pairs ...string) (map[string]string, error) { + length, err := checkPairs(pairs...) + if err != nil { + return nil, err + } + m := make(map[string]string, length/2) + for i := 0; i < length; i += 2 { + m[pairs[i]] = pairs[i+1] + } + return m, nil +} + +// mapFromPairsToRegex converts variadic string parameters to a +// string to regex map. +func mapFromPairsToRegex(pairs ...string) (map[string]*regexp.Regexp, error) { + length, err := checkPairs(pairs...) + if err != nil { + return nil, err + } + m := make(map[string]*regexp.Regexp, length/2) + for i := 0; i < length; i += 2 { + regex, err := regexp.Compile(pairs[i+1]) + if err != nil { + return nil, err + } + m[pairs[i]] = regex + } + return m, nil +} + +// matchInArray returns true if the given string value is in the array. +func matchInArray(arr []string, value string) bool { + for _, v := range arr { + if v == value { + return true + } + } + return false +} + +// matchMapWithString returns true if the given key/value pairs exist in a given map. +func matchMapWithString(toCheck map[string]string, toMatch map[string][]string, canonicalKey bool) bool { + for k, v := range toCheck { + // Check if key exists. + if canonicalKey { + k = http.CanonicalHeaderKey(k) + } + if values := toMatch[k]; values == nil { + return false + } else if v != "" { + // If value was defined as an empty string we only check that the + // key exists. Otherwise we also check for equality. + valueExists := false + for _, value := range values { + if v == value { + valueExists = true + break + } + } + if !valueExists { + return false + } + } + } + return true +} + +// matchMapWithRegex returns true if the given key/value pairs exist in a given map compiled against +// the given regex +func matchMapWithRegex(toCheck map[string]*regexp.Regexp, toMatch map[string][]string, canonicalKey bool) bool { + for k, v := range toCheck { + // Check if key exists. + if canonicalKey { + k = http.CanonicalHeaderKey(k) + } + if values := toMatch[k]; values == nil { + return false + } else if v != nil { + // If value was defined as an empty string we only check that the + // key exists. Otherwise we also check for equality. + valueExists := false + for _, value := range values { + if v.MatchString(value) { + valueExists = true + break + } + } + if !valueExists { + return false + } + } + } + return true +} + +// methodNotAllowed replies to the request with an HTTP status code 405. +func methodNotAllowed(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusMethodNotAllowed) +} + +// methodNotAllowedHandler returns a simple request handler +// that replies to each request with a status code 405. +func methodNotAllowedHandler() http.Handler { return http.HandlerFunc(methodNotAllowed) } diff --git a/vendor/github.com/gorilla/mux/regexp.go b/vendor/github.com/gorilla/mux/regexp.go new file mode 100644 index 00000000..5d05cfa0 --- /dev/null +++ b/vendor/github.com/gorilla/mux/regexp.go @@ -0,0 +1,388 @@ +// Copyright 2012 The Gorilla 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 mux + +import ( + "bytes" + "fmt" + "net/http" + "net/url" + "regexp" + "strconv" + "strings" +) + +type routeRegexpOptions struct { + strictSlash bool + useEncodedPath bool +} + +type regexpType int + +const ( + regexpTypePath regexpType = iota + regexpTypeHost + regexpTypePrefix + regexpTypeQuery +) + +// newRouteRegexp parses a route template and returns a routeRegexp, +// used to match a host, a path or a query string. +// +// It will extract named variables, assemble a regexp to be matched, create +// a "reverse" template to build URLs and compile regexps to validate variable +// values used in URL building. +// +// Previously we accepted only Python-like identifiers for variable +// names ([a-zA-Z_][a-zA-Z0-9_]*), but currently the only restriction is that +// name and pattern can't be empty, and names can't contain a colon. +func newRouteRegexp(tpl string, typ regexpType, options routeRegexpOptions) (*routeRegexp, error) { + // Check if it is well-formed. + idxs, errBraces := braceIndices(tpl) + if errBraces != nil { + return nil, errBraces + } + // Backup the original. + template := tpl + // Now let's parse it. + defaultPattern := "[^/]+" + if typ == regexpTypeQuery { + defaultPattern = ".*" + } else if typ == regexpTypeHost { + defaultPattern = "[^.]+" + } + // Only match strict slash if not matching + if typ != regexpTypePath { + options.strictSlash = false + } + // Set a flag for strictSlash. + endSlash := false + if options.strictSlash && strings.HasSuffix(tpl, "/") { + tpl = tpl[:len(tpl)-1] + endSlash = true + } + varsN := make([]string, len(idxs)/2) + varsR := make([]*regexp.Regexp, len(idxs)/2) + pattern := bytes.NewBufferString("") + pattern.WriteByte('^') + reverse := bytes.NewBufferString("") + var end int + var err error + for i := 0; i < len(idxs); i += 2 { + // Set all values we are interested in. + raw := tpl[end:idxs[i]] + end = idxs[i+1] + parts := strings.SplitN(tpl[idxs[i]+1:end-1], ":", 2) + name := parts[0] + patt := defaultPattern + if len(parts) == 2 { + patt = parts[1] + } + // Name or pattern can't be empty. + if name == "" || patt == "" { + return nil, fmt.Errorf("mux: missing name or pattern in %q", + tpl[idxs[i]:end]) + } + // Build the regexp pattern. + fmt.Fprintf(pattern, "%s(?P<%s>%s)", regexp.QuoteMeta(raw), varGroupName(i/2), patt) + + // Build the reverse template. + fmt.Fprintf(reverse, "%s%%s", raw) + + // Append variable name and compiled pattern. + varsN[i/2] = name + varsR[i/2], err = regexp.Compile(fmt.Sprintf("^%s$", patt)) + if err != nil { + return nil, err + } + } + // Add the remaining. + raw := tpl[end:] + pattern.WriteString(regexp.QuoteMeta(raw)) + if options.strictSlash { + pattern.WriteString("[/]?") + } + if typ == regexpTypeQuery { + // Add the default pattern if the query value is empty + if queryVal := strings.SplitN(template, "=", 2)[1]; queryVal == "" { + pattern.WriteString(defaultPattern) + } + } + if typ != regexpTypePrefix { + pattern.WriteByte('$') + } + + var wildcardHostPort bool + if typ == regexpTypeHost { + if !strings.Contains(pattern.String(), ":") { + wildcardHostPort = true + } + } + reverse.WriteString(raw) + if endSlash { + reverse.WriteByte('/') + } + // Compile full regexp. + reg, errCompile := regexp.Compile(pattern.String()) + if errCompile != nil { + return nil, errCompile + } + + // Check for capturing groups which used to work in older versions + if reg.NumSubexp() != len(idxs)/2 { + panic(fmt.Sprintf("route %s contains capture groups in its regexp. ", template) + + "Only non-capturing groups are accepted: e.g. (?:pattern) instead of (pattern)") + } + + // Done! + return &routeRegexp{ + template: template, + regexpType: typ, + options: options, + regexp: reg, + reverse: reverse.String(), + varsN: varsN, + varsR: varsR, + wildcardHostPort: wildcardHostPort, + }, nil +} + +// routeRegexp stores a regexp to match a host or path and information to +// collect and validate route variables. +type routeRegexp struct { + // The unmodified template. + template string + // The type of match + regexpType regexpType + // Options for matching + options routeRegexpOptions + // Expanded regexp. + regexp *regexp.Regexp + // Reverse template. + reverse string + // Variable names. + varsN []string + // Variable regexps (validators). + varsR []*regexp.Regexp + // Wildcard host-port (no strict port match in hostname) + wildcardHostPort bool +} + +// Match matches the regexp against the URL host or path. +func (r *routeRegexp) Match(req *http.Request, match *RouteMatch) bool { + if r.regexpType == regexpTypeHost { + host := getHost(req) + if r.wildcardHostPort { + // Don't be strict on the port match + if i := strings.Index(host, ":"); i != -1 { + host = host[:i] + } + } + return r.regexp.MatchString(host) + } + + if r.regexpType == regexpTypeQuery { + return r.matchQueryString(req) + } + path := req.URL.Path + if r.options.useEncodedPath { + path = req.URL.EscapedPath() + } + return r.regexp.MatchString(path) +} + +// url builds a URL part using the given values. +func (r *routeRegexp) url(values map[string]string) (string, error) { + urlValues := make([]interface{}, len(r.varsN)) + for k, v := range r.varsN { + value, ok := values[v] + if !ok { + return "", fmt.Errorf("mux: missing route variable %q", v) + } + if r.regexpType == regexpTypeQuery { + value = url.QueryEscape(value) + } + urlValues[k] = value + } + rv := fmt.Sprintf(r.reverse, urlValues...) + if !r.regexp.MatchString(rv) { + // The URL is checked against the full regexp, instead of checking + // individual variables. This is faster but to provide a good error + // message, we check individual regexps if the URL doesn't match. + for k, v := range r.varsN { + if !r.varsR[k].MatchString(values[v]) { + return "", fmt.Errorf( + "mux: variable %q doesn't match, expected %q", values[v], + r.varsR[k].String()) + } + } + } + return rv, nil +} + +// getURLQuery returns a single query parameter from a request URL. +// For a URL with foo=bar&baz=ding, we return only the relevant key +// value pair for the routeRegexp. +func (r *routeRegexp) getURLQuery(req *http.Request) string { + if r.regexpType != regexpTypeQuery { + return "" + } + templateKey := strings.SplitN(r.template, "=", 2)[0] + val, ok := findFirstQueryKey(req.URL.RawQuery, templateKey) + if ok { + return templateKey + "=" + val + } + return "" +} + +// findFirstQueryKey returns the same result as (*url.URL).Query()[key][0]. +// If key was not found, empty string and false is returned. +func findFirstQueryKey(rawQuery, key string) (value string, ok bool) { + query := []byte(rawQuery) + for len(query) > 0 { + foundKey := query + if i := bytes.IndexAny(foundKey, "&;"); i >= 0 { + foundKey, query = foundKey[:i], foundKey[i+1:] + } else { + query = query[:0] + } + if len(foundKey) == 0 { + continue + } + var value []byte + if i := bytes.IndexByte(foundKey, '='); i >= 0 { + foundKey, value = foundKey[:i], foundKey[i+1:] + } + if len(foundKey) < len(key) { + // Cannot possibly be key. + continue + } + keyString, err := url.QueryUnescape(string(foundKey)) + if err != nil { + continue + } + if keyString != key { + continue + } + valueString, err := url.QueryUnescape(string(value)) + if err != nil { + continue + } + return valueString, true + } + return "", false +} + +func (r *routeRegexp) matchQueryString(req *http.Request) bool { + return r.regexp.MatchString(r.getURLQuery(req)) +} + +// braceIndices returns the first level curly brace indices from a string. +// It returns an error in case of unbalanced braces. +func braceIndices(s string) ([]int, error) { + var level, idx int + var idxs []int + for i := 0; i < len(s); i++ { + switch s[i] { + case '{': + if level++; level == 1 { + idx = i + } + case '}': + if level--; level == 0 { + idxs = append(idxs, idx, i+1) + } else if level < 0 { + return nil, fmt.Errorf("mux: unbalanced braces in %q", s) + } + } + } + if level != 0 { + return nil, fmt.Errorf("mux: unbalanced braces in %q", s) + } + return idxs, nil +} + +// varGroupName builds a capturing group name for the indexed variable. +func varGroupName(idx int) string { + return "v" + strconv.Itoa(idx) +} + +// ---------------------------------------------------------------------------- +// routeRegexpGroup +// ---------------------------------------------------------------------------- + +// routeRegexpGroup groups the route matchers that carry variables. +type routeRegexpGroup struct { + host *routeRegexp + path *routeRegexp + queries []*routeRegexp +} + +// setMatch extracts the variables from the URL once a route matches. +func (v routeRegexpGroup) setMatch(req *http.Request, m *RouteMatch, r *Route) { + // Store host variables. + if v.host != nil { + host := getHost(req) + if v.host.wildcardHostPort { + // Don't be strict on the port match + if i := strings.Index(host, ":"); i != -1 { + host = host[:i] + } + } + matches := v.host.regexp.FindStringSubmatchIndex(host) + if len(matches) > 0 { + extractVars(host, matches, v.host.varsN, m.Vars) + } + } + path := req.URL.Path + if r.useEncodedPath { + path = req.URL.EscapedPath() + } + // Store path variables. + if v.path != nil { + matches := v.path.regexp.FindStringSubmatchIndex(path) + if len(matches) > 0 { + extractVars(path, matches, v.path.varsN, m.Vars) + // Check if we should redirect. + if v.path.options.strictSlash { + p1 := strings.HasSuffix(path, "/") + p2 := strings.HasSuffix(v.path.template, "/") + if p1 != p2 { + u, _ := url.Parse(req.URL.String()) + if p1 { + u.Path = u.Path[:len(u.Path)-1] + } else { + u.Path += "/" + } + m.Handler = http.RedirectHandler(u.String(), http.StatusMovedPermanently) + } + } + } + } + // Store query string variables. + for _, q := range v.queries { + queryURL := q.getURLQuery(req) + matches := q.regexp.FindStringSubmatchIndex(queryURL) + if len(matches) > 0 { + extractVars(queryURL, matches, q.varsN, m.Vars) + } + } +} + +// getHost tries its best to return the request host. +// According to section 14.23 of RFC 2616 the Host header +// can include the port number if the default value of 80 is not used. +func getHost(r *http.Request) string { + if r.URL.IsAbs() { + return r.URL.Host + } + return r.Host +} + +func extractVars(input string, matches []int, names []string, output map[string]string) { + for i, name := range names { + output[name] = input[matches[2*i+2]:matches[2*i+3]] + } +} diff --git a/vendor/github.com/gorilla/mux/route.go b/vendor/github.com/gorilla/mux/route.go new file mode 100644 index 00000000..e8f11df2 --- /dev/null +++ b/vendor/github.com/gorilla/mux/route.go @@ -0,0 +1,765 @@ +// Copyright 2012 The Gorilla 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 mux + +import ( + "errors" + "fmt" + "net/http" + "net/url" + "regexp" + "strings" +) + +// Route stores information to match a request and build URLs. +type Route struct { + // Request handler for the route. + handler http.Handler + // If true, this route never matches: it is only used to build URLs. + buildOnly bool + // The name used to build URLs. + name string + // Error resulted from building a route. + err error + + // "global" reference to all named routes + namedRoutes map[string]*Route + + // config possibly passed in from `Router` + routeConf +} + +// SkipClean reports whether path cleaning is enabled for this route via +// Router.SkipClean. +func (r *Route) SkipClean() bool { + return r.skipClean +} + +// Match matches the route against the request. +func (r *Route) Match(req *http.Request, match *RouteMatch) bool { + if r.buildOnly || r.err != nil { + return false + } + + var matchErr error + + // Match everything. + for _, m := range r.matchers { + if matched := m.Match(req, match); !matched { + if _, ok := m.(methodMatcher); ok { + matchErr = ErrMethodMismatch + continue + } + + // Ignore ErrNotFound errors. These errors arise from match call + // to Subrouters. + // + // This prevents subsequent matching subrouters from failing to + // run middleware. If not ignored, the middleware would see a + // non-nil MatchErr and be skipped, even when there was a + // matching route. + if match.MatchErr == ErrNotFound { + match.MatchErr = nil + } + + matchErr = nil // nolint:ineffassign + return false + } else { + // Multiple routes may share the same path but use different HTTP methods. For instance: + // Route 1: POST "/users/{id}". + // Route 2: GET "/users/{id}", parameters: "id": "[0-9]+". + // + // The router must handle these cases correctly. For a GET request to "/users/abc" with "id" as "-2", + // The router should return a "Not Found" error as no route fully matches this request. + if match.MatchErr == ErrMethodMismatch { + match.MatchErr = nil + } + } + } + + if matchErr != nil { + match.MatchErr = matchErr + return false + } + + if match.MatchErr == ErrMethodMismatch && r.handler != nil { + // We found a route which matches request method, clear MatchErr + match.MatchErr = nil + // Then override the mis-matched handler + match.Handler = r.handler + } + + // Yay, we have a match. Let's collect some info about it. + if match.Route == nil { + match.Route = r + } + if match.Handler == nil { + match.Handler = r.handler + } + if match.Vars == nil { + match.Vars = make(map[string]string) + } + + // Set variables. + r.regexp.setMatch(req, match, r) + return true +} + +// ---------------------------------------------------------------------------- +// Route attributes +// ---------------------------------------------------------------------------- + +// GetError returns an error resulted from building the route, if any. +func (r *Route) GetError() error { + return r.err +} + +// BuildOnly sets the route to never match: it is only used to build URLs. +func (r *Route) BuildOnly() *Route { + r.buildOnly = true + return r +} + +// Handler -------------------------------------------------------------------- + +// Handler sets a handler for the route. +func (r *Route) Handler(handler http.Handler) *Route { + if r.err == nil { + r.handler = handler + } + return r +} + +// HandlerFunc sets a handler function for the route. +func (r *Route) HandlerFunc(f func(http.ResponseWriter, *http.Request)) *Route { + return r.Handler(http.HandlerFunc(f)) +} + +// GetHandler returns the handler for the route, if any. +func (r *Route) GetHandler() http.Handler { + return r.handler +} + +// Name ----------------------------------------------------------------------- + +// Name sets the name for the route, used to build URLs. +// It is an error to call Name more than once on a route. +func (r *Route) Name(name string) *Route { + if r.name != "" { + r.err = fmt.Errorf("mux: route already has name %q, can't set %q", + r.name, name) + } + if r.err == nil { + r.name = name + r.namedRoutes[name] = r + } + return r +} + +// GetName returns the name for the route, if any. +func (r *Route) GetName() string { + return r.name +} + +// ---------------------------------------------------------------------------- +// Matchers +// ---------------------------------------------------------------------------- + +// matcher types try to match a request. +type matcher interface { + Match(*http.Request, *RouteMatch) bool +} + +// addMatcher adds a matcher to the route. +func (r *Route) addMatcher(m matcher) *Route { + if r.err == nil { + r.matchers = append(r.matchers, m) + } + return r +} + +// addRegexpMatcher adds a host or path matcher and builder to a route. +func (r *Route) addRegexpMatcher(tpl string, typ regexpType) error { + if r.err != nil { + return r.err + } + if typ == regexpTypePath || typ == regexpTypePrefix { + if len(tpl) > 0 && tpl[0] != '/' { + return fmt.Errorf("mux: path must start with a slash, got %q", tpl) + } + if r.regexp.path != nil { + tpl = strings.TrimRight(r.regexp.path.template, "/") + tpl + } + } + rr, err := newRouteRegexp(tpl, typ, routeRegexpOptions{ + strictSlash: r.strictSlash, + useEncodedPath: r.useEncodedPath, + }) + if err != nil { + return err + } + for _, q := range r.regexp.queries { + if err = uniqueVars(rr.varsN, q.varsN); err != nil { + return err + } + } + if typ == regexpTypeHost { + if r.regexp.path != nil { + if err = uniqueVars(rr.varsN, r.regexp.path.varsN); err != nil { + return err + } + } + r.regexp.host = rr + } else { + if r.regexp.host != nil { + if err = uniqueVars(rr.varsN, r.regexp.host.varsN); err != nil { + return err + } + } + if typ == regexpTypeQuery { + r.regexp.queries = append(r.regexp.queries, rr) + } else { + r.regexp.path = rr + } + } + r.addMatcher(rr) + return nil +} + +// Headers -------------------------------------------------------------------- + +// headerMatcher matches the request against header values. +type headerMatcher map[string]string + +func (m headerMatcher) Match(r *http.Request, match *RouteMatch) bool { + return matchMapWithString(m, r.Header, true) +} + +// Headers adds a matcher for request header values. +// It accepts a sequence of key/value pairs to be matched. For example: +// +// r := mux.NewRouter().NewRoute() +// r.Headers("Content-Type", "application/json", +// "X-Requested-With", "XMLHttpRequest") +// +// The above route will only match if both request header values match. +// If the value is an empty string, it will match any value if the key is set. +func (r *Route) Headers(pairs ...string) *Route { + if r.err == nil { + var headers map[string]string + headers, r.err = mapFromPairsToString(pairs...) + return r.addMatcher(headerMatcher(headers)) + } + return r +} + +// headerRegexMatcher matches the request against the route given a regex for the header +type headerRegexMatcher map[string]*regexp.Regexp + +func (m headerRegexMatcher) Match(r *http.Request, match *RouteMatch) bool { + return matchMapWithRegex(m, r.Header, true) +} + +// HeadersRegexp accepts a sequence of key/value pairs, where the value has regex +// support. For example: +// +// r := mux.NewRouter().NewRoute() +// r.HeadersRegexp("Content-Type", "application/(text|json)", +// "X-Requested-With", "XMLHttpRequest") +// +// The above route will only match if both the request header matches both regular expressions. +// If the value is an empty string, it will match any value if the key is set. +// Use the start and end of string anchors (^ and $) to match an exact value. +func (r *Route) HeadersRegexp(pairs ...string) *Route { + if r.err == nil { + var headers map[string]*regexp.Regexp + headers, r.err = mapFromPairsToRegex(pairs...) + return r.addMatcher(headerRegexMatcher(headers)) + } + return r +} + +// Host ----------------------------------------------------------------------- + +// Host adds a matcher for the URL host. +// It accepts a template with zero or more URL variables enclosed by {}. +// Variables can define an optional regexp pattern to be matched: +// +// - {name} matches anything until the next dot. +// +// - {name:pattern} matches the given regexp pattern. +// +// For example: +// +// r := mux.NewRouter().NewRoute() +// r.Host("www.example.com") +// r.Host("{subdomain}.domain.com") +// r.Host("{subdomain:[a-z]+}.domain.com") +// +// Variable names must be unique in a given route. They can be retrieved +// calling mux.Vars(request). +func (r *Route) Host(tpl string) *Route { + r.err = r.addRegexpMatcher(tpl, regexpTypeHost) + return r +} + +// MatcherFunc ---------------------------------------------------------------- + +// MatcherFunc is the function signature used by custom matchers. +type MatcherFunc func(*http.Request, *RouteMatch) bool + +// Match returns the match for a given request. +func (m MatcherFunc) Match(r *http.Request, match *RouteMatch) bool { + return m(r, match) +} + +// MatcherFunc adds a custom function to be used as request matcher. +func (r *Route) MatcherFunc(f MatcherFunc) *Route { + return r.addMatcher(f) +} + +// Methods -------------------------------------------------------------------- + +// methodMatcher matches the request against HTTP methods. +type methodMatcher []string + +func (m methodMatcher) Match(r *http.Request, match *RouteMatch) bool { + return matchInArray(m, r.Method) +} + +// Methods adds a matcher for HTTP methods. +// It accepts a sequence of one or more methods to be matched, e.g.: +// "GET", "POST", "PUT". +func (r *Route) Methods(methods ...string) *Route { + for k, v := range methods { + methods[k] = strings.ToUpper(v) + } + return r.addMatcher(methodMatcher(methods)) +} + +// Path ----------------------------------------------------------------------- + +// Path adds a matcher for the URL path. +// It accepts a template with zero or more URL variables enclosed by {}. The +// template must start with a "/". +// Variables can define an optional regexp pattern to be matched: +// +// - {name} matches anything until the next slash. +// +// - {name:pattern} matches the given regexp pattern. +// +// For example: +// +// r := mux.NewRouter().NewRoute() +// r.Path("/products/").Handler(ProductsHandler) +// r.Path("/products/{key}").Handler(ProductsHandler) +// r.Path("/articles/{category}/{id:[0-9]+}"). +// Handler(ArticleHandler) +// +// Variable names must be unique in a given route. They can be retrieved +// calling mux.Vars(request). +func (r *Route) Path(tpl string) *Route { + r.err = r.addRegexpMatcher(tpl, regexpTypePath) + return r +} + +// PathPrefix ----------------------------------------------------------------- + +// PathPrefix adds a matcher for the URL path prefix. This matches if the given +// template is a prefix of the full URL path. See Route.Path() for details on +// the tpl argument. +// +// Note that it does not treat slashes specially ("/foobar/" will be matched by +// the prefix "/foo") so you may want to use a trailing slash here. +// +// Also note that the setting of Router.StrictSlash() has no effect on routes +// with a PathPrefix matcher. +func (r *Route) PathPrefix(tpl string) *Route { + r.err = r.addRegexpMatcher(tpl, regexpTypePrefix) + return r +} + +// Query ---------------------------------------------------------------------- + +// Queries adds a matcher for URL query values. +// It accepts a sequence of key/value pairs. Values may define variables. +// For example: +// +// r := mux.NewRouter().NewRoute() +// r.Queries("foo", "bar", "id", "{id:[0-9]+}") +// +// The above route will only match if the URL contains the defined queries +// values, e.g.: ?foo=bar&id=42. +// +// If the value is an empty string, it will match any value if the key is set. +// +// Variables can define an optional regexp pattern to be matched: +// +// - {name} matches anything until the next slash. +// +// - {name:pattern} matches the given regexp pattern. +func (r *Route) Queries(pairs ...string) *Route { + length := len(pairs) + if length%2 != 0 { + r.err = fmt.Errorf( + "mux: number of parameters must be multiple of 2, got %v", pairs) + return nil + } + for i := 0; i < length; i += 2 { + if r.err = r.addRegexpMatcher(pairs[i]+"="+pairs[i+1], regexpTypeQuery); r.err != nil { + return r + } + } + + return r +} + +// Schemes -------------------------------------------------------------------- + +// schemeMatcher matches the request against URL schemes. +type schemeMatcher []string + +func (m schemeMatcher) Match(r *http.Request, match *RouteMatch) bool { + scheme := r.URL.Scheme + // https://golang.org/pkg/net/http/#Request + // "For [most] server requests, fields other than Path and RawQuery will be + // empty." + // Since we're an http muxer, the scheme is either going to be http or https + // though, so we can just set it based on the tls termination state. + if scheme == "" { + if r.TLS == nil { + scheme = "http" + } else { + scheme = "https" + } + } + return matchInArray(m, scheme) +} + +// Schemes adds a matcher for URL schemes. +// It accepts a sequence of schemes to be matched, e.g.: "http", "https". +// If the request's URL has a scheme set, it will be matched against. +// Generally, the URL scheme will only be set if a previous handler set it, +// such as the ProxyHeaders handler from gorilla/handlers. +// If unset, the scheme will be determined based on the request's TLS +// termination state. +// The first argument to Schemes will be used when constructing a route URL. +func (r *Route) Schemes(schemes ...string) *Route { + for k, v := range schemes { + schemes[k] = strings.ToLower(v) + } + if len(schemes) > 0 { + r.buildScheme = schemes[0] + } + return r.addMatcher(schemeMatcher(schemes)) +} + +// BuildVarsFunc -------------------------------------------------------------- + +// BuildVarsFunc is the function signature used by custom build variable +// functions (which can modify route variables before a route's URL is built). +type BuildVarsFunc func(map[string]string) map[string]string + +// BuildVarsFunc adds a custom function to be used to modify build variables +// before a route's URL is built. +func (r *Route) BuildVarsFunc(f BuildVarsFunc) *Route { + if r.buildVarsFunc != nil { + // compose the old and new functions + old := r.buildVarsFunc + r.buildVarsFunc = func(m map[string]string) map[string]string { + return f(old(m)) + } + } else { + r.buildVarsFunc = f + } + return r +} + +// Subrouter ------------------------------------------------------------------ + +// Subrouter creates a subrouter for the route. +// +// It will test the inner routes only if the parent route matched. For example: +// +// r := mux.NewRouter().NewRoute() +// s := r.Host("www.example.com").Subrouter() +// s.HandleFunc("/products/", ProductsHandler) +// s.HandleFunc("/products/{key}", ProductHandler) +// s.HandleFunc("/articles/{category}/{id:[0-9]+}"), ArticleHandler) +// +// Here, the routes registered in the subrouter won't be tested if the host +// doesn't match. +func (r *Route) Subrouter() *Router { + // initialize a subrouter with a copy of the parent route's configuration + router := &Router{routeConf: copyRouteConf(r.routeConf), namedRoutes: r.namedRoutes} + r.addMatcher(router) + return router +} + +// ---------------------------------------------------------------------------- +// URL building +// ---------------------------------------------------------------------------- + +// URL builds a URL for the route. +// +// It accepts a sequence of key/value pairs for the route variables. For +// example, given this route: +// +// r := mux.NewRouter() +// r.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler). +// Name("article") +// +// ...a URL for it can be built using: +// +// url, err := r.Get("article").URL("category", "technology", "id", "42") +// +// ...which will return an url.URL with the following path: +// +// "/articles/technology/42" +// +// This also works for host variables: +// +// r := mux.NewRouter() +// r.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler). +// Host("{subdomain}.domain.com"). +// Name("article") +// +// // url.String() will be "http://news.domain.com/articles/technology/42" +// url, err := r.Get("article").URL("subdomain", "news", +// "category", "technology", +// "id", "42") +// +// The scheme of the resulting url will be the first argument that was passed to Schemes: +// +// // url.String() will be "https://example.com" +// r := mux.NewRouter().NewRoute() +// url, err := r.Host("example.com") +// .Schemes("https", "http").URL() +// +// All variables defined in the route are required, and their values must +// conform to the corresponding patterns. +func (r *Route) URL(pairs ...string) (*url.URL, error) { + if r.err != nil { + return nil, r.err + } + values, err := r.prepareVars(pairs...) + if err != nil { + return nil, err + } + var scheme, host, path string + queries := make([]string, 0, len(r.regexp.queries)) + if r.regexp.host != nil { + if host, err = r.regexp.host.url(values); err != nil { + return nil, err + } + scheme = "http" + if r.buildScheme != "" { + scheme = r.buildScheme + } + } + if r.regexp.path != nil { + if path, err = r.regexp.path.url(values); err != nil { + return nil, err + } + } + for _, q := range r.regexp.queries { + var query string + if query, err = q.url(values); err != nil { + return nil, err + } + queries = append(queries, query) + } + return &url.URL{ + Scheme: scheme, + Host: host, + Path: path, + RawQuery: strings.Join(queries, "&"), + }, nil +} + +// URLHost builds the host part of the URL for a route. See Route.URL(). +// +// The route must have a host defined. +func (r *Route) URLHost(pairs ...string) (*url.URL, error) { + if r.err != nil { + return nil, r.err + } + if r.regexp.host == nil { + return nil, errors.New("mux: route doesn't have a host") + } + values, err := r.prepareVars(pairs...) + if err != nil { + return nil, err + } + host, err := r.regexp.host.url(values) + if err != nil { + return nil, err + } + u := &url.URL{ + Scheme: "http", + Host: host, + } + if r.buildScheme != "" { + u.Scheme = r.buildScheme + } + return u, nil +} + +// URLPath builds the path part of the URL for a route. See Route.URL(). +// +// The route must have a path defined. +func (r *Route) URLPath(pairs ...string) (*url.URL, error) { + if r.err != nil { + return nil, r.err + } + if r.regexp.path == nil { + return nil, errors.New("mux: route doesn't have a path") + } + values, err := r.prepareVars(pairs...) + if err != nil { + return nil, err + } + path, err := r.regexp.path.url(values) + if err != nil { + return nil, err + } + return &url.URL{ + Path: path, + }, nil +} + +// GetPathTemplate returns the template used to build the +// route match. +// This is useful for building simple REST API documentation and for instrumentation +// against third-party services. +// An error will be returned if the route does not define a path. +func (r *Route) GetPathTemplate() (string, error) { + if r.err != nil { + return "", r.err + } + if r.regexp.path == nil { + return "", errors.New("mux: route doesn't have a path") + } + return r.regexp.path.template, nil +} + +// GetPathRegexp returns the expanded regular expression used to match route path. +// This is useful for building simple REST API documentation and for instrumentation +// against third-party services. +// An error will be returned if the route does not define a path. +func (r *Route) GetPathRegexp() (string, error) { + if r.err != nil { + return "", r.err + } + if r.regexp.path == nil { + return "", errors.New("mux: route does not have a path") + } + return r.regexp.path.regexp.String(), nil +} + +// GetQueriesRegexp returns the expanded regular expressions used to match the +// route queries. +// This is useful for building simple REST API documentation and for instrumentation +// against third-party services. +// An error will be returned if the route does not have queries. +func (r *Route) GetQueriesRegexp() ([]string, error) { + if r.err != nil { + return nil, r.err + } + if r.regexp.queries == nil { + return nil, errors.New("mux: route doesn't have queries") + } + queries := make([]string, 0, len(r.regexp.queries)) + for _, query := range r.regexp.queries { + queries = append(queries, query.regexp.String()) + } + return queries, nil +} + +// GetQueriesTemplates returns the templates used to build the +// query matching. +// This is useful for building simple REST API documentation and for instrumentation +// against third-party services. +// An error will be returned if the route does not define queries. +func (r *Route) GetQueriesTemplates() ([]string, error) { + if r.err != nil { + return nil, r.err + } + if r.regexp.queries == nil { + return nil, errors.New("mux: route doesn't have queries") + } + queries := make([]string, 0, len(r.regexp.queries)) + for _, query := range r.regexp.queries { + queries = append(queries, query.template) + } + return queries, nil +} + +// GetMethods returns the methods the route matches against +// This is useful for building simple REST API documentation and for instrumentation +// against third-party services. +// An error will be returned if route does not have methods. +func (r *Route) GetMethods() ([]string, error) { + if r.err != nil { + return nil, r.err + } + for _, m := range r.matchers { + if methods, ok := m.(methodMatcher); ok { + return []string(methods), nil + } + } + return nil, errors.New("mux: route doesn't have methods") +} + +// GetHostTemplate returns the template used to build the +// route match. +// This is useful for building simple REST API documentation and for instrumentation +// against third-party services. +// An error will be returned if the route does not define a host. +func (r *Route) GetHostTemplate() (string, error) { + if r.err != nil { + return "", r.err + } + if r.regexp.host == nil { + return "", errors.New("mux: route doesn't have a host") + } + return r.regexp.host.template, nil +} + +// GetVarNames returns the names of all variables added by regexp matchers +// These can be used to know which route variables should be passed into r.URL() +func (r *Route) GetVarNames() ([]string, error) { + if r.err != nil { + return nil, r.err + } + var varNames []string + if r.regexp.host != nil { + varNames = append(varNames, r.regexp.host.varsN...) + } + if r.regexp.path != nil { + varNames = append(varNames, r.regexp.path.varsN...) + } + for _, regx := range r.regexp.queries { + varNames = append(varNames, regx.varsN...) + } + return varNames, nil +} + +// prepareVars converts the route variable pairs into a map. If the route has a +// BuildVarsFunc, it is invoked. +func (r *Route) prepareVars(pairs ...string) (map[string]string, error) { + m, err := mapFromPairsToString(pairs...) + if err != nil { + return nil, err + } + return r.buildVars(m), nil +} + +func (r *Route) buildVars(m map[string]string) map[string]string { + if r.buildVarsFunc != nil { + m = r.buildVarsFunc(m) + } + return m +} diff --git a/vendor/github.com/gorilla/mux/test_helpers.go b/vendor/github.com/gorilla/mux/test_helpers.go new file mode 100644 index 00000000..5f5c496d --- /dev/null +++ b/vendor/github.com/gorilla/mux/test_helpers.go @@ -0,0 +1,19 @@ +// Copyright 2012 The Gorilla 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 mux + +import "net/http" + +// SetURLVars sets the URL variables for the given request, to be accessed via +// mux.Vars for testing route behaviour. Arguments are not modified, a shallow +// copy is returned. +// +// This API should only be used for testing purposes; it provides a way to +// inject variables into the request context. Alternatively, URL variables +// can be set by making a route that captures the required variables, +// starting a server and sending the request to that server. +func SetURLVars(r *http.Request, val map[string]string) *http.Request { + return requestWithVars(r, val) +} diff --git a/vendor/github.com/mattn/go-isatty/isatty_bsd.go b/vendor/github.com/mattn/go-isatty/isatty_bsd.go index d569c0c9..d0ea68f4 100644 --- a/vendor/github.com/mattn/go-isatty/isatty_bsd.go +++ b/vendor/github.com/mattn/go-isatty/isatty_bsd.go @@ -1,6 +1,7 @@ -//go:build (darwin || freebsd || openbsd || netbsd || dragonfly || hurd) && !appengine +//go:build (darwin || freebsd || openbsd || netbsd || dragonfly || hurd) && !appengine && !tinygo // +build darwin freebsd openbsd netbsd dragonfly hurd // +build !appengine +// +build !tinygo package isatty diff --git a/vendor/github.com/mattn/go-isatty/isatty_others.go b/vendor/github.com/mattn/go-isatty/isatty_others.go index 31503226..7402e061 100644 --- a/vendor/github.com/mattn/go-isatty/isatty_others.go +++ b/vendor/github.com/mattn/go-isatty/isatty_others.go @@ -1,5 +1,6 @@ -//go:build appengine || js || nacl || wasm -// +build appengine js nacl wasm +//go:build (appengine || js || nacl || tinygo || wasm) && !windows +// +build appengine js nacl tinygo wasm +// +build !windows package isatty diff --git a/vendor/github.com/mattn/go-isatty/isatty_tcgets.go b/vendor/github.com/mattn/go-isatty/isatty_tcgets.go index 67787657..0337d8cf 100644 --- a/vendor/github.com/mattn/go-isatty/isatty_tcgets.go +++ b/vendor/github.com/mattn/go-isatty/isatty_tcgets.go @@ -1,6 +1,7 @@ -//go:build (linux || aix || zos) && !appengine +//go:build (linux || aix || zos) && !appengine && !tinygo // +build linux aix zos // +build !appengine +// +build !tinygo package isatty diff --git a/vendor/github.com/microcosm-cc/bluemonday/helpers.go b/vendor/github.com/microcosm-cc/bluemonday/helpers.go index 2b03d7e7..aa0b7b92 100644 --- a/vendor/github.com/microcosm-cc/bluemonday/helpers.go +++ b/vendor/github.com/microcosm-cc/bluemonday/helpers.go @@ -222,11 +222,7 @@ func (p *Policy) AllowDataURIImages() { } _, err := base64.StdEncoding.DecodeString(url.Opaque[len(matched):]) - if err != nil { - return false - } - - return true + return err == nil }, ) } diff --git a/vendor/github.com/microcosm-cc/bluemonday/policy.go b/vendor/github.com/microcosm-cc/bluemonday/policy.go index 1a5e00ce..b4f09879 100644 --- a/vendor/github.com/microcosm-cc/bluemonday/policy.go +++ b/vendor/github.com/microcosm-cc/bluemonday/policy.go @@ -117,6 +117,19 @@ type Policy struct { // returning true are allowed. allowURLSchemes map[string][]urlPolicy + // These regexps are used to match allowed URL schemes, for example + // if one would want to allow all URL schemes, they would add `.+`. + // However pay attention as this can lead to XSS being rendered thus + // defeating the purpose of using a HTML sanitizer. + // The regexps are only considered if a schema was not explicitly + // handled by `AllowURLSchemes` or `AllowURLSchemeWithCustomPolicy`. + allowURLSchemeRegexps []*regexp.Regexp + + // If srcRewriter is not nil, it is used to rewrite the src attribute + // of tags that download resources, such as and + + diff --git a/internal/dev_server/ui/package-lock.json b/internal/dev_server/ui/package-lock.json new file mode 100644 index 00000000..e64e671f --- /dev/null +++ b/internal/dev_server/ui/package-lock.json @@ -0,0 +1,3255 @@ +{ + "name": "ldcli-dev-server-ui", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "ldcli-dev-server-ui", + "version": "0.0.0", + "dependencies": { + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", + "@typescript-eslint/eslint-plugin": "^7.13.1", + "@typescript-eslint/parser": "^7.13.1", + "@vitejs/plugin-react": "^4.3.1", + "eslint": "^8.57.0", + "eslint-plugin-react-hooks": "^4.6.2", + "eslint-plugin-react-refresh": "^0.4.7", + "typescript": "^5.2.2", + "vite": "^5.3.1" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz", + "integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.24.7", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.7.tgz", + "integrity": "sha512-qJzAIcv03PyaWqxRgO4mSU3lihncDT296vnyuE2O8uA4w3UHWI4S3hgeZd1L8W1Bft40w9JxJ2b412iDUFFRhw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.7.tgz", + "integrity": "sha512-nykK+LEK86ahTkX/3TgauT0ikKoNCfKHEaZYTUVupJdTLzGNvrblu4u6fa7DhZONAltdf8e662t/abY8idrd/g==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.24.7", + "@babel/helper-compilation-targets": "^7.24.7", + "@babel/helper-module-transforms": "^7.24.7", + "@babel/helpers": "^7.24.7", + "@babel/parser": "^7.24.7", + "@babel/template": "^7.24.7", + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.7.tgz", + "integrity": "sha512-oipXieGC3i45Y1A41t4tAqpnEZWgB/lC6Ehh6+rOviR5XWpTtMmLN+fGjz9vOiNRt0p6RtO6DtD0pdU3vpqdSA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.24.7", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.24.7.tgz", + "integrity": "sha512-ctSdRHBi20qWOfy27RUb4Fhp07KSJ3sXcuSvTrXrc4aG8NSYDo1ici3Vhg9bg69y5bj0Mr1lh0aeEgTvc12rMg==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.24.7", + "@babel/helper-validator-option": "^7.24.7", + "browserslist": "^4.22.2", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-environment-visitor": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.24.7.tgz", + "integrity": "sha512-DoiN84+4Gnd0ncbBOM9AZENV4a5ZiL39HYMyZJGZ/AZEykHYdJw0wW3kdcsh9/Kn+BRXHLkkklZ51ecPKmI1CQ==", + "dev": true, + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-function-name": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.24.7.tgz", + "integrity": "sha512-FyoJTsj/PEUWu1/TYRiXTIHc8lbw+TDYkZuoE43opPS5TrI7MyONBE1oNvfguEXAD9yhQRrVBnXdXzSLQl9XnA==", + "dev": true, + "dependencies": { + "@babel/template": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-hoist-variables": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.24.7.tgz", + "integrity": "sha512-MJJwhkoGy5c4ehfoRyrJ/owKeMl19U54h27YYftT0o2teQ3FJ3nQUf/I3LlJsX4l3qlw7WRXUmiyajvHXoTubQ==", + "dev": true, + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz", + "integrity": "sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.24.7.tgz", + "integrity": "sha512-1fuJEwIrp+97rM4RWdO+qrRsZlAeL1lQJoPqtCYWv0NL115XM93hIH4CSRln2w52SqvmY5hqdtauB6QFCDiZNQ==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-module-imports": "^7.24.7", + "@babel/helper-simple-access": "^7.24.7", + "@babel/helper-split-export-declaration": "^7.24.7", + "@babel/helper-validator-identifier": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.7.tgz", + "integrity": "sha512-Rq76wjt7yz9AAc1KnlRKNAi/dMSVWgDRx43FHoJEbcYU6xOWaE2dVPwcdTukJrjxS65GITyfbvEYHvkirZ6uEg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.7.tgz", + "integrity": "sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.7.tgz", + "integrity": "sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.7.tgz", + "integrity": "sha512-7MbVt6xrwFQbunH2DNQsAP5sTGxfqQtErvBIvIMi6EQnbgUOuVYanvREcmFrOPhoXBrTtjhhP+lW+o5UfK+tDg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", + "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.7.tgz", + "integrity": "sha512-yy1/KvjhV/ZCL+SM7hBrvnZJ3ZuT9OuZgIJAGpPEToANvc3iM6iDvBnRjtElWibHU6n8/LPR/EjX9EtIEYO3pw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.7.tgz", + "integrity": "sha512-NlmJJtvcw72yRJRcnCmGvSi+3jDEg8qFu3z0AFoymmzLx5ERVWyzd9kVXr7Th9/8yIJi2Zc6av4Tqz3wFs8QWg==", + "dev": true, + "dependencies": { + "@babel/template": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz", + "integrity": "sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.24.7", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.7.tgz", + "integrity": "sha512-9uUYRm6OqQrCqQdG1iCBwBPZgN8ciDBro2nIOFaiRz1/BCxaI7CNvQbDHvsArAC7Tw9Hda/B3U+6ui9u4HWXPw==", + "dev": true, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.24.7.tgz", + "integrity": "sha512-fOPQYbGSgH0HUp4UJO4sMBFjY6DuWq+2i8rixyUMb3CdGixs/gccURvYOAhajBdKDoGajFr3mUq5rH3phtkGzw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.24.7.tgz", + "integrity": "sha512-J2z+MWzZHVOemyLweMqngXrgGC42jQ//R0KdxqkIz/OrbVIIlhFI3WigZ5fO+nwFvBlncr4MGapd8vTyc7RPNQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.7.tgz", + "integrity": "sha512-jYqfPrU9JTF0PmPy1tLYHW4Mp4KlgxJD9l2nP9fD6yT/ICi554DmrWBAEYpIelzjHf1msDP3PxJIRt/nFNfBig==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.24.7", + "@babel/parser": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.7.tgz", + "integrity": "sha512-yb65Ed5S/QAcewNPh0nZczy9JdYXkkAbIsEo+P7BE7yO3txAY30Y/oPa3QkQ5It3xVG2kpKMg9MsdxZaO31uKA==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.24.7", + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-function-name": "^7.24.7", + "@babel/helper-hoist-variables": "^7.24.7", + "@babel/helper-split-export-declaration": "^7.24.7", + "@babel/parser": "^7.24.7", + "@babel/types": "^7.24.7", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.7.tgz", + "integrity": "sha512-XEFXSlxiG5td2EJRe8vOmRbaXVgfcBlszKujvVmWIK/UpywWljQCfzAv3RQCGujWQ1RD4YYWEAqDXfuJiy8f5Q==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.24.7", + "@babel/helper-validator-identifier": "^7.24.7", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.1.tgz", + "integrity": "sha512-Zm2NGpWELsQAD1xsJzGQpYfvICSsFkEpU0jxBjfdC6uNEWXcHnfs9hScFWtXVDVl+rBQJGrl4g1vcKIejpH9dA==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", + "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.11.14", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", + "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", + "deprecated": "Use @eslint/config-array instead", + "dev": true, + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.2", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", + "dev": true + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.18.0.tgz", + "integrity": "sha512-Tya6xypR10giZV1XzxmH5wr25VcZSncG0pZIjfePT0OVBvqNEurzValetGNarVrGiq66EBVAFn15iYX4w6FKgQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.18.0.tgz", + "integrity": "sha512-avCea0RAP03lTsDhEyfy+hpfr85KfyTctMADqHVhLAF3MlIkq83CP8UfAHUssgXTYd+6er6PaAhx/QGv4L1EiA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.18.0.tgz", + "integrity": "sha512-IWfdwU7KDSm07Ty0PuA/W2JYoZ4iTj3TUQjkVsO/6U+4I1jN5lcR71ZEvRh52sDOERdnNhhHU57UITXz5jC1/w==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.18.0.tgz", + "integrity": "sha512-n2LMsUz7Ynu7DoQrSQkBf8iNrjOGyPLrdSg802vk6XT3FtsgX6JbE8IHRvposskFm9SNxzkLYGSq9QdpLYpRNA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.18.0.tgz", + "integrity": "sha512-C/zbRYRXFjWvz9Z4haRxcTdnkPt1BtCkz+7RtBSuNmKzMzp3ZxdM28Mpccn6pt28/UWUCTXa+b0Mx1k3g6NOMA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.18.0.tgz", + "integrity": "sha512-l3m9ewPgjQSXrUMHg93vt0hYCGnrMOcUpTz6FLtbwljo2HluS4zTXFy2571YQbisTnfTKPZ01u/ukJdQTLGh9A==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.18.0.tgz", + "integrity": "sha512-rJ5D47d8WD7J+7STKdCUAgmQk49xuFrRi9pZkWoRD1UeSMakbcepWXPF8ycChBoAqs1pb2wzvbY6Q33WmN2ftw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.18.0.tgz", + "integrity": "sha512-be6Yx37b24ZwxQ+wOQXXLZqpq4jTckJhtGlWGZs68TgdKXJgw54lUUoFYrg6Zs/kjzAQwEwYbp8JxZVzZLRepQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.18.0.tgz", + "integrity": "sha512-hNVMQK+qrA9Todu9+wqrXOHxFiD5YmdEi3paj6vP02Kx1hjd2LLYR2eaN7DsEshg09+9uzWi2W18MJDlG0cxJA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.18.0.tgz", + "integrity": "sha512-ROCM7i+m1NfdrsmvwSzoxp9HFtmKGHEqu5NNDiZWQtXLA8S5HBCkVvKAxJ8U+CVctHwV2Gb5VUaK7UAkzhDjlg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.18.0.tgz", + "integrity": "sha512-0UyyRHyDN42QL+NbqevXIIUnKA47A+45WyasO+y2bGJ1mhQrfrtXUpTxCOrfxCR4esV3/RLYyucGVPiUsO8xjg==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.18.0.tgz", + "integrity": "sha512-xuglR2rBVHA5UsI8h8UbX4VJ470PtGCf5Vpswh7p2ukaqBGFTnsfzxUBetoWBWymHMxbIG0Cmx7Y9qDZzr648w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.18.0.tgz", + "integrity": "sha512-LKaqQL9osY/ir2geuLVvRRs+utWUNilzdE90TpyoX0eNqPzWjRm14oMEE+YLve4k/NAqCdPkGYDaDF5Sw+xBfg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.18.0.tgz", + "integrity": "sha512-7J6TkZQFGo9qBKH0pk2cEVSRhJbL6MtfWxth7Y5YmZs57Pi+4x6c2dStAUvaQkHQLnEQv1jzBUW43GvZW8OFqA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.18.0.tgz", + "integrity": "sha512-Txjh+IxBPbkUB9+SXZMpv+b/vnTEtFyfWZgJ6iyCmt2tdx0OF5WhFowLmnh8ENGNpfUlUZkdI//4IEmhwPieNg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.18.0.tgz", + "integrity": "sha512-UOo5FdvOL0+eIVTgS4tIdbW+TtnBLWg1YBCcU2KWM7nuNwRz9bksDX1bekJJCpu25N1DVWaCwnT39dVQxzqS8g==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.6.8", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", + "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.20.6", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.6.tgz", + "integrity": "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.20.7" + } + }, + "node_modules/@types/estree": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "dev": true + }, + "node_modules/@types/prop-types": { + "version": "15.7.12", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz", + "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==", + "dev": true + }, + "node_modules/@types/react": { + "version": "18.3.3", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.3.tgz", + "integrity": "sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==", + "dev": true, + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.0", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.0.tgz", + "integrity": "sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==", + "dev": true, + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.14.1.tgz", + "integrity": "sha512-aAJd6bIf2vvQRjUG3ZkNXkmBpN+J7Wd0mfQiiVCJMu9Z5GcZZdcc0j8XwN/BM97Fl7e3SkTXODSk4VehUv7CGw==", + "dev": true, + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "7.14.1", + "@typescript-eslint/type-utils": "7.14.1", + "@typescript-eslint/utils": "7.14.1", + "@typescript-eslint/visitor-keys": "7.14.1", + "graphemer": "^1.4.0", + "ignore": "^5.3.1", + "natural-compare": "^1.4.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^7.0.0", + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.14.1.tgz", + "integrity": "sha512-8lKUOebNLcR0D7RvlcloOacTOWzOqemWEWkKSVpMZVF/XVcwjPR+3MD08QzbW9TCGJ+DwIc6zUSGZ9vd8cO1IA==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "7.14.1", + "@typescript-eslint/types": "7.14.1", + "@typescript-eslint/typescript-estree": "7.14.1", + "@typescript-eslint/visitor-keys": "7.14.1", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.14.1.tgz", + "integrity": "sha512-gPrFSsoYcsffYXTOZ+hT7fyJr95rdVe4kGVX1ps/dJ+DfmlnjFN/GcMxXcVkeHDKqsq6uAcVaQaIi3cFffmAbA==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.14.1", + "@typescript-eslint/visitor-keys": "7.14.1" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.14.1.tgz", + "integrity": "sha512-/MzmgNd3nnbDbOi3LfasXWWe292+iuo+umJ0bCCMCPc1jLO/z2BQmWUUUXvXLbrQey/JgzdF/OV+I5bzEGwJkQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/typescript-estree": "7.14.1", + "@typescript-eslint/utils": "7.14.1", + "debug": "^4.3.4", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.14.1.tgz", + "integrity": "sha512-mL7zNEOQybo5R3AavY+Am7KLv8BorIv7HCYS5rKoNZKQD9tsfGUpO4KdAn3sSUvTiS4PQkr2+K0KJbxj8H9NDg==", + "dev": true, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.14.1.tgz", + "integrity": "sha512-k5d0VuxViE2ulIO6FbxxSZaxqDVUyMbXcidC8rHvii0I56XZPv8cq+EhMns+d/EVIL41sMXqRbK3D10Oza1bbA==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.14.1", + "@typescript-eslint/visitor-keys": "7.14.1", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.14.1.tgz", + "integrity": "sha512-CMmVVELns3nak3cpJhZosDkm63n+DwBlDX8g0k4QUa9BMnF+lH2lr3d130M1Zt1xxmB3LLk3NV7KQCq86ZBBhQ==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "7.14.1", + "@typescript-eslint/types": "7.14.1", + "@typescript-eslint/typescript-estree": "7.14.1" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.14.1.tgz", + "integrity": "sha512-Crb+F75U1JAEtBeQGxSKwI60hZmmzaqA3z9sYsVm8X7W5cwLEm5bRe0/uXS6+MR/y8CVpKSR/ontIAIEPFcEkA==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.14.1", + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", + "dev": true + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.3.1.tgz", + "integrity": "sha512-m/V2syj5CuVnaxcUJOQRel/Wr31FFXRFlnOoq1TVtkCxsY5veGMTEmpWHndrhB2U8ScHtCQB1e+4hWYExQc6Lg==", + "dev": true, + "dependencies": { + "@babel/core": "^7.24.5", + "@babel/plugin-transform-react-jsx-self": "^7.24.5", + "@babel/plugin-transform-react-jsx-source": "^7.24.1", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.14.2" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0" + } + }, + "node_modules/acorn": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.0.tgz", + "integrity": "sha512-RTvkC4w+KNXrM39/lWCUaG0IbRkWdCv7W/IOW9oU6SawyxulvkQy5HQPVTKxEjczcUvapcrw3cFx/60VN/NRNw==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.23.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.1.tgz", + "integrity": "sha512-TUfofFo/KsK/bWZ9TWQ5O26tsWW4Uhmt8IYklbnUa70udB6P2wA7w7o4PY4muaEPBQaAX+CEnmmIA41NVHtPVw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001629", + "electron-to-chromium": "^1.4.796", + "node-releases": "^2.0.14", + "update-browserslist-db": "^1.0.16" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001637", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001637.tgz", + "integrity": "sha512-1x0qRI1mD1o9e+7mBI7XtzFAP4XszbHaVWsMiGbSPLYekKTJF7K+FNk6AsXH4sUpc+qrsI3pVgf1Jdl/uGkuSQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "dev": true + }, + "node_modules/debug": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.4.812", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.812.tgz", + "integrity": "sha512-7L8fC2Ey/b6SePDFKR2zHAy4mbdp1/38Yk5TsARO66W3hC5KEaeKMMHoxwtuH+jcu2AYLSn9QX04i95t6Fl1Hg==", + "dev": true + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", + "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/eslint": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", + "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.0", + "@humanwhocodes/config-array": "^0.11.14", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz", + "integrity": "sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.4.7", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.7.tgz", + "integrity": "sha512-yrj+KInFmwuQS2UQcg1SF83ha1tuHC1jMQbRNyuWtlEzzKRDgAl7L4Yp4NlDUZTZNlWvHEzOtJhMi40R7JxcSw==", + "dev": true, + "peerDependencies": { + "eslint": ">=7" + } + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/eslint/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/eslint/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/eslint/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/eslint/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", + "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "node_modules/fastq": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", + "dev": true + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/ignore": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", + "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz", + "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==", + "dev": true, + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "node_modules/node-releases": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", + "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", + "dev": true + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", + "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==", + "dev": true + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.4.38", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", + "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.0.0", + "source-map-js": "^1.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-refresh": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", + "integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rollup": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.18.0.tgz", + "integrity": "sha512-QmJz14PX3rzbJCN1SG4Xe/bAAX2a6NpCP8ab2vfu2GiUr8AQcr2nCV/oEO3yneFarB67zk8ShlIyWb2LGTb3Sg==", + "dev": true, + "dependencies": { + "@types/estree": "1.0.5" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.18.0", + "@rollup/rollup-android-arm64": "4.18.0", + "@rollup/rollup-darwin-arm64": "4.18.0", + "@rollup/rollup-darwin-x64": "4.18.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.18.0", + "@rollup/rollup-linux-arm-musleabihf": "4.18.0", + "@rollup/rollup-linux-arm64-gnu": "4.18.0", + "@rollup/rollup-linux-arm64-musl": "4.18.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.18.0", + "@rollup/rollup-linux-riscv64-gnu": "4.18.0", + "@rollup/rollup-linux-s390x-gnu": "4.18.0", + "@rollup/rollup-linux-x64-gnu": "4.18.0", + "@rollup/rollup-linux-x64-musl": "4.18.0", + "@rollup/rollup-win32-arm64-msvc": "4.18.0", + "@rollup/rollup-win32-ia32-msvc": "4.18.0", + "@rollup/rollup-win32-x64-msvc": "4.18.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map-js": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", + "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-api-utils": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", + "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==", + "dev": true, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.5.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.2.tgz", + "integrity": "sha512-NcRtPEOsPFFWjobJEtfihkLCZCXZt/os3zf8nTxjVH3RvTSxjrCamJpbExGvYOF+tFHc3pA65qpdwPbzjohhew==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.16.tgz", + "integrity": "sha512-KVbTxlBYlckhF5wgfyZXTWnMn7MMZjMu9XG8bPlliUOP9ThaF4QnhP8qrjrH7DRzHfSk0oQv1wToW+iA5GajEQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.1.2", + "picocolors": "^1.0.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/vite": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.3.1.tgz", + "integrity": "sha512-XBmSKRLXLxiaPYamLv3/hnP/KXDai1NDexN0FpkTaZXTfycHvkRHoenpgl/fvuK/kPbB6xAgoyiryAhQNxYmAQ==", + "dev": true, + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.38", + "rollup": "^4.13.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/internal/dev_server/ui/package.json b/internal/dev_server/ui/package.json new file mode 100644 index 00000000..b28fd35d --- /dev/null +++ b/internal/dev_server/ui/package.json @@ -0,0 +1,28 @@ +{ + "name": "ldcli-dev-server-ui", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", + "preview": "vite preview" + }, + "dependencies": { + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", + "@typescript-eslint/eslint-plugin": "^7.13.1", + "@typescript-eslint/parser": "^7.13.1", + "@vitejs/plugin-react": "^4.3.1", + "eslint": "^8.57.0", + "eslint-plugin-react-hooks": "^4.6.2", + "eslint-plugin-react-refresh": "^0.4.7", + "typescript": "^5.2.2", + "vite": "^5.3.1" + } +} diff --git a/internal/dev_server/ui/public/vite.svg b/internal/dev_server/ui/public/vite.svg new file mode 100644 index 00000000..e7b8dfb1 --- /dev/null +++ b/internal/dev_server/ui/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/internal/dev_server/ui/src/App.css b/internal/dev_server/ui/src/App.css new file mode 100644 index 00000000..b9d355df --- /dev/null +++ b/internal/dev_server/ui/src/App.css @@ -0,0 +1,42 @@ +#root { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; +} + +.logo { + height: 6em; + padding: 1.5em; + will-change: filter; + transition: filter 300ms; +} +.logo:hover { + filter: drop-shadow(0 0 2em #646cffaa); +} +.logo.react:hover { + filter: drop-shadow(0 0 2em #61dafbaa); +} + +@keyframes logo-spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +@media (prefers-reduced-motion: no-preference) { + a:nth-of-type(2) .logo { + animation: logo-spin infinite 20s linear; + } +} + +.card { + padding: 2em; +} + +.read-the-docs { + color: #888; +} diff --git a/internal/dev_server/ui/src/App.tsx b/internal/dev_server/ui/src/App.tsx new file mode 100644 index 00000000..1f554812 --- /dev/null +++ b/internal/dev_server/ui/src/App.tsx @@ -0,0 +1,35 @@ +import { useState } from 'react' +import reactLogo from './assets/react.svg' +import viteLogo from '/vite.svg' +import './App.css' + +function App() { + const [count, setCount] = useState(0) + + return ( + <> + +

Vite + React

+
+ +

+ Edit src/App.tsx and save to test HMR +

+
+

+ Click on the Vite and React logos to learn more +

+ + ) +} + +export default App diff --git a/internal/dev_server/ui/src/assets/react.svg b/internal/dev_server/ui/src/assets/react.svg new file mode 100644 index 00000000..6c87de9b --- /dev/null +++ b/internal/dev_server/ui/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/internal/dev_server/ui/src/index.css b/internal/dev_server/ui/src/index.css new file mode 100644 index 00000000..6119ad9a --- /dev/null +++ b/internal/dev_server/ui/src/index.css @@ -0,0 +1,68 @@ +:root { + font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; + line-height: 1.5; + font-weight: 400; + + color-scheme: light dark; + color: rgba(255, 255, 255, 0.87); + background-color: #242424; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +a { + font-weight: 500; + color: #646cff; + text-decoration: inherit; +} +a:hover { + color: #535bf2; +} + +body { + margin: 0; + display: flex; + place-items: center; + min-width: 320px; + min-height: 100vh; +} + +h1 { + font-size: 3.2em; + line-height: 1.1; +} + +button { + border-radius: 8px; + border: 1px solid transparent; + padding: 0.6em 1.2em; + font-size: 1em; + font-weight: 500; + font-family: inherit; + background-color: #1a1a1a; + cursor: pointer; + transition: border-color 0.25s; +} +button:hover { + border-color: #646cff; +} +button:focus, +button:focus-visible { + outline: 4px auto -webkit-focus-ring-color; +} + +@media (prefers-color-scheme: light) { + :root { + color: #213547; + background-color: #ffffff; + } + a:hover { + color: #747bff; + } + button { + background-color: #f9f9f9; + } +} diff --git a/internal/dev_server/ui/src/main.tsx b/internal/dev_server/ui/src/main.tsx new file mode 100644 index 00000000..3d7150da --- /dev/null +++ b/internal/dev_server/ui/src/main.tsx @@ -0,0 +1,10 @@ +import React from 'react' +import ReactDOM from 'react-dom/client' +import App from './App.tsx' +import './index.css' + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + , +) diff --git a/internal/dev_server/ui/src/vite-env.d.ts b/internal/dev_server/ui/src/vite-env.d.ts new file mode 100644 index 00000000..11f02fe2 --- /dev/null +++ b/internal/dev_server/ui/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/internal/dev_server/ui/tsconfig.app.json b/internal/dev_server/ui/tsconfig.app.json new file mode 100644 index 00000000..d739292a --- /dev/null +++ b/internal/dev_server/ui/tsconfig.app.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "composite": true, + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"] +} diff --git a/internal/dev_server/ui/tsconfig.json b/internal/dev_server/ui/tsconfig.json new file mode 100644 index 00000000..ea9d0cd8 --- /dev/null +++ b/internal/dev_server/ui/tsconfig.json @@ -0,0 +1,11 @@ +{ + "files": [], + "references": [ + { + "path": "./tsconfig.app.json" + }, + { + "path": "./tsconfig.node.json" + } + ] +} diff --git a/internal/dev_server/ui/tsconfig.node.json b/internal/dev_server/ui/tsconfig.node.json new file mode 100644 index 00000000..3afdd6e3 --- /dev/null +++ b/internal/dev_server/ui/tsconfig.node.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "composite": true, + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true, + "strict": true, + "noEmit": true + }, + "include": ["vite.config.ts"] +} diff --git a/internal/dev_server/ui/vite.config.ts b/internal/dev_server/ui/vite.config.ts new file mode 100644 index 00000000..5a33944a --- /dev/null +++ b/internal/dev_server/ui/vite.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [react()], +}) From 9e66b53db92d2fbfaf1ac09e3b16af30467f0b33 Mon Sep 17 00:00:00 2001 From: Kerrie Martinez Date: Wed, 26 Jun 2024 13:29:42 -0700 Subject: [PATCH 33/79] Add flag state (#337) * adding flag state to the project response * forgot to add flagstate to post response --- internal/dev_server/api/api.yaml | 70 +++++++++++++++------------ internal/dev_server/api/server.gen.go | 7 +++ internal/dev_server/api/server.go | 2 + 3 files changed, 47 insertions(+), 32 deletions(-) diff --git a/internal/dev_server/api/api.yaml b/internal/dev_server/api/api.yaml index 445f5010..834841da 100644 --- a/internal/dev_server/api/api.yaml +++ b/internal/dev_server/api/api.yaml @@ -7,7 +7,7 @@ info: those flags. version: 1.0.0 servers: - - url: 'http' + - url: "http" paths: /dev/projects: get: @@ -27,17 +27,17 @@ paths: get: summary: get the specified project and its configuration for syncing from the LaunchDarkly Service parameters: - - $ref: '#/components/parameters/projectKey' + - $ref: "#/components/parameters/projectKey" responses: 200: - $ref: '#/components/responses/Project' + $ref: "#/components/responses/Project" 404: description: No project found delete: summary: remove the specified project from the dev server parameters: - - $ref: '#/components/parameters/projectKey' + - $ref: "#/components/parameters/projectKey" responses: 204: description: OK. Project & overrides were removed @@ -46,7 +46,7 @@ paths: post: summary: Add the project to the dev server parameters: - - $ref: '#/components/parameters/projectKey' + - $ref: "#/components/parameters/projectKey" requestBody: content: application/json: @@ -67,48 +67,48 @@ paths: # kind: user responses: 201: - $ref: '#/components/responses/Project' -# WIP updates can be added later -# patch: -# summary: change configuration of the project -# parameters: -# - $ref: '#/components/parameters/projectKey' -# responses: + $ref: "#/components/responses/Project" + # WIP updates can be added later + # patch: + # summary: change configuration of the project + # parameters: + # - $ref: '#/components/parameters/projectKey' + # responses: -# WIP NOT SURE IF NEEDED TBH -# /dev/projects/{projectKey}/overrides: -# get: -# summary: return all the flag overrides for the project -# parameters: -# - $ref: '#/components/parameters/projectKey' -# responses: // WIP -# delete: -# summary: clear all overrides for the project -# parameters: -# - $ref: '#/components/parameters/projectKey' -# responses: // WIP + # WIP NOT SURE IF NEEDED TBH + # /dev/projects/{projectKey}/overrides: + # get: + # summary: return all the flag overrides for the project + # parameters: + # - $ref: '#/components/parameters/projectKey' + # responses: // WIP + # delete: + # summary: clear all overrides for the project + # parameters: + # - $ref: '#/components/parameters/projectKey' + # responses: // WIP /dev/projects/{projectKey}/overrides/{flagKey}: put: summary: override flag value with value provided in the body parameters: - - $ref: '#/components/parameters/projectKey' - - $ref: '#/components/parameters/flagKey' + - $ref: "#/components/parameters/projectKey" + - $ref: "#/components/parameters/flagKey" requestBody: required: true description: flag value to override flag with. The json representation of the variation value. content: application/json: schema: - $ref: '#/components/schemas/FlagValue' + $ref: "#/components/schemas/FlagValue" responses: 200: - $ref: '#/components/responses/FlagOverride' + $ref: "#/components/responses/FlagOverride" delete: summary: remove override for flag parameters: - - $ref: '#/components/parameters/projectKey' - - $ref: '#/components/parameters/flagKey' + - $ref: "#/components/parameters/projectKey" + - $ref: "#/components/parameters/flagKey" responses: 204: description: OK. override removed @@ -151,7 +151,7 @@ components: - value properties: value: - $ref: '#/components/schemas/FlagValue' + $ref: "#/components/schemas/FlagValue" override: type: boolean description: whether or not this is an overridden value or one from the source environment @@ -177,7 +177,13 @@ components: kind: user x-go-type: ldcontext.Context x-go-type-import: - path: github.com/launchdarkly/go-sdk-common/v3/ldcontext + path: github.com/launchdarkly/go-sdk-common/v3/ldcontext + flagsState: + type: object + description: flags and their values and version for a given project in the source environment + x-go-type: model.FlagsState + x-go-type-import: + path: github.com/launchdarkly/ldcli/internal/dev_server/model _lastSyncedFromSource: type: integer x-go-type: int64 diff --git a/internal/dev_server/api/server.gen.go b/internal/dev_server/api/server.gen.go index e754c878..293605a7 100644 --- a/internal/dev_server/api/server.gen.go +++ b/internal/dev_server/api/server.gen.go @@ -12,6 +12,7 @@ import ( "github.com/gorilla/mux" "github.com/launchdarkly/go-sdk-common/v3/ldcontext" "github.com/launchdarkly/go-sdk-common/v3/ldvalue" + "github.com/launchdarkly/ldcli/internal/dev_server/model" "github.com/oapi-codegen/runtime" strictnethttp "github.com/oapi-codegen/runtime/strictmiddleware/nethttp" ) @@ -42,6 +43,9 @@ type Project struct { // Context context object to use when evaluating flags in source environment Context ldcontext.Context `json:"context"` + // FlagsState flags and their values and version for a given project in the source environment + FlagsState *model.FlagsState `json:"flagsState,omitempty"` + // SourceEnvironmentKey environment to copy flag values from SourceEnvironmentKey string `json:"sourceEnvironmentKey"` } @@ -395,6 +399,9 @@ type ProjectJSONResponse struct { // Context context object to use when evaluating flags in source environment Context ldcontext.Context `json:"context"` + // FlagsState flags and their values and version for a given project in the source environment + FlagsState *model.FlagsState `json:"flagsState,omitempty"` + // SourceEnvironmentKey environment to copy flag values from SourceEnvironmentKey string `json:"sourceEnvironmentKey"` } diff --git a/internal/dev_server/api/server.go b/internal/dev_server/api/server.go index eab5e8b1..fc78d142 100644 --- a/internal/dev_server/api/server.go +++ b/internal/dev_server/api/server.go @@ -54,6 +54,7 @@ func (s Server) GetDevProjectsProjectKey(ctx context.Context, request GetDevProj LastSyncedFromSource: project.LastSyncTime.Unix(), Context: project.Context, SourceEnvironmentKey: project.SourceEnvironmentKey, + FlagsState: &project.FlagState, }, }, nil } @@ -68,6 +69,7 @@ func (s Server) PostDevProjectsProjectKey(ctx context.Context, request PostDevPr LastSyncedFromSource: project.LastSyncTime.Unix(), Context: project.Context, SourceEnvironmentKey: project.SourceEnvironmentKey, + FlagsState: &project.FlagState, }, }, nil } From a555de4b367d89cf4645334ecf0a595b8998ddb5 Mon Sep 17 00:00:00 2001 From: Keegan Watkins <109103953+kwatkins-ld@users.noreply.github.com> Date: Wed, 26 Jun 2024 15:34:52 -0500 Subject: [PATCH 34/79] feat: add Launchpad and begin to port Gonfalon overrides UI (#339) --- internal/dev_server/ui/.npmrc | 1 + internal/dev_server/ui/README.md | 8 +- internal/dev_server/ui/index.html | 2 +- internal/dev_server/ui/package-lock.json | 4290 +++++++++++++++++-- internal/dev_server/ui/package.json | 32 +- internal/dev_server/ui/src/App.css | 57 +- internal/dev_server/ui/src/App.tsx | 97 +- internal/dev_server/ui/src/IconProvider.tsx | 28 + internal/dev_server/ui/src/assets/react.svg | 1 - internal/dev_server/ui/src/index.css | 68 - internal/dev_server/ui/src/main.tsx | 16 +- internal/dev_server/ui/src/vite-env.d.ts | 1 + internal/dev_server/ui/vite.config.ts | 9 +- 13 files changed, 4186 insertions(+), 424 deletions(-) create mode 100644 internal/dev_server/ui/.npmrc create mode 100644 internal/dev_server/ui/src/IconProvider.tsx delete mode 100644 internal/dev_server/ui/src/assets/react.svg delete mode 100644 internal/dev_server/ui/src/index.css diff --git a/internal/dev_server/ui/.npmrc b/internal/dev_server/ui/.npmrc new file mode 100644 index 00000000..449691b7 --- /dev/null +++ b/internal/dev_server/ui/.npmrc @@ -0,0 +1 @@ +save-exact=true \ No newline at end of file diff --git a/internal/dev_server/ui/README.md b/internal/dev_server/ui/README.md index 0d6babed..bb156850 100644 --- a/internal/dev_server/ui/README.md +++ b/internal/dev_server/ui/README.md @@ -17,12 +17,12 @@ If you are developing a production application, we recommend updating the config export default { // other rules... parserOptions: { - ecmaVersion: 'latest', - sourceType: 'module', - project: ['./tsconfig.json', './tsconfig.node.json'], + ecmaVersion: "latest", + sourceType: "module", + project: ["./tsconfig.json", "./tsconfig.node.json"], tsconfigRootDir: __dirname, }, -} +}; ``` - Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked` diff --git a/internal/dev_server/ui/index.html b/internal/dev_server/ui/index.html index e4b78eae..dc5ddc8c 100644 --- a/internal/dev_server/ui/index.html +++ b/internal/dev_server/ui/index.html @@ -1,5 +1,5 @@ - + diff --git a/internal/dev_server/ui/package-lock.json b/internal/dev_server/ui/package-lock.json index e64e671f..72980a3b 100644 --- a/internal/dev_server/ui/package-lock.json +++ b/internal/dev_server/ui/package-lock.json @@ -8,20 +8,27 @@ "name": "ldcli-dev-server-ui", "version": "0.0.0", "dependencies": { - "react": "^18.3.1", - "react-dom": "^18.3.1" + "@launchpad-ui/components": "0.2.12", + "@launchpad-ui/core": "0.49.22", + "@launchpad-ui/icons": "0.18.4", + "@launchpad-ui/tokens": "0.9.12", + "launchdarkly-js-client-sdk": "3.4.0", + "react": "18.3.1", + "react-dom": "18.3.1" }, "devDependencies": { - "@types/react": "^18.3.3", - "@types/react-dom": "^18.3.0", - "@typescript-eslint/eslint-plugin": "^7.13.1", - "@typescript-eslint/parser": "^7.13.1", - "@vitejs/plugin-react": "^4.3.1", - "eslint": "^8.57.0", - "eslint-plugin-react-hooks": "^4.6.2", - "eslint-plugin-react-refresh": "^0.4.7", - "typescript": "^5.2.2", - "vite": "^5.3.1" + "@types/react": "18.3.3", + "@types/react-dom": "18.3.0", + "@typescript-eslint/eslint-plugin": "7.13.1", + "@typescript-eslint/parser": "7.13.1", + "@vitejs/plugin-react": "4.3.1", + "eslint": "8.57.0", + "eslint-plugin-react-hooks": "4.6.2", + "eslint-plugin-react-refresh": "0.4.7", + "prettier": "3.3.2", + "typescript": "5.2.2", + "vite": "5.3.1", + "vite-plugin-svgr": "^4.2.0" } }, "node_modules/@ampproject/remapping": { @@ -338,6 +345,17 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/runtime": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.7.tgz", + "integrity": "sha512-UwgBRMjJP+xv857DCngvqXI3Iq6J4v0wXmwc6sapg+zyhbwmQX67LUEFrkK5tbyJ30jGuG3ZvWpBiB9LCy1kWw==", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.7.tgz", @@ -387,6 +405,12 @@ "node": ">=6.9.0" } }, + "node_modules/@emotion/hash": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.1.tgz", + "integrity": "sha512-gJB6HLm5rYwSLI6PQa+X1t5CFGrv1J1TWG+sOyMCeKz2ojaj6Fnl/rZEspogG+cvqbt4AE/2eIyD2QfLKTBNlQ==", + "peer": true + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", @@ -848,6 +872,72 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@floating-ui/core": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.0.tgz", + "integrity": "sha512-PcF++MykgmTj3CIyOQbKA/hDzOAiqI3mhuoN44WRCopIs1sgoDoU4oty4Jtqaj/y3oDU6fnVSm4QG0a3t5i0+g==", + "dependencies": { + "@floating-ui/utils": "^0.2.1" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.1.tgz", + "integrity": "sha512-iA8qE43/H5iGozC3W0YSnVSW42Vh522yyM1gj+BqRwVsTNOyr231PsXDaV04yT39PsO0QL2QpbI/M0ZaLUQgRQ==", + "dependencies": { + "@floating-ui/core": "^1.6.0", + "@floating-ui/utils": "^0.2.1" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.3.tgz", + "integrity": "sha512-XGndio0l5/Gvd6CLIABvsav9HHezgDFFhDfHk1bvLfr9ni8dojqLSvBbotJEjmIwNHL7vK4QzBJTdBRoB+c1ww==" + }, + "node_modules/@formatjs/ecma402-abstract": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-2.0.0.tgz", + "integrity": "sha512-rRqXOqdFmk7RYvj4khklyqzcfQl9vEL/usogncBHRZfZBDOwMGuSRNFl02fu5KGHXdbinju+YXyuR+Nk8xlr/g==", + "dependencies": { + "@formatjs/intl-localematcher": "0.5.4", + "tslib": "^2.4.0" + } + }, + "node_modules/@formatjs/fast-memoize": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-2.2.0.tgz", + "integrity": "sha512-hnk/nY8FyrL5YxwP9e4r9dqeM6cAbo8PeU9UjyXojZMNvVad2Z06FAVHyR3Ecw6fza+0GH7vdJgiKIVXTMbSBA==", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@formatjs/icu-messageformat-parser": { + "version": "2.7.8", + "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.7.8.tgz", + "integrity": "sha512-nBZJYmhpcSX0WeJ5SDYUkZ42AgR3xiyhNCsQweFx3cz/ULJjym8bHAzWKvG5e2+1XO98dBYC0fWeeAECAVSwLA==", + "dependencies": { + "@formatjs/ecma402-abstract": "2.0.0", + "@formatjs/icu-skeleton-parser": "1.8.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@formatjs/icu-skeleton-parser": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.8.2.tgz", + "integrity": "sha512-k4ERKgw7aKGWJZgTarIcNEmvyTVD9FYh0mTrrBMHZ1b8hUu6iOJ4SzsZlo3UNAvHYa+PnvntIwRPt1/vy4nA9Q==", + "dependencies": { + "@formatjs/ecma402-abstract": "2.0.0", + "tslib": "^2.4.0" + } + }, + "node_modules/@formatjs/intl-localematcher": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.5.4.tgz", + "integrity": "sha512-zTwEpWOzZ2CiKcB93BLngUX59hQkuZjT2+SAQEscSm52peDW/getsawMcWF1rGRpMCX6D7nSJA3CzJ8gn13N/g==", + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.14", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", @@ -905,6 +995,39 @@ "deprecated": "Use @eslint/object-schema instead", "dev": true }, + "node_modules/@internationalized/date": { + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.5.4.tgz", + "integrity": "sha512-qoVJVro+O0rBaw+8HPjUB1iH8Ihf8oziEnqMnvhJUSuVIrHOuZ6eNLHNvzXJKUvAtaDiqMnRlg8Z2mgh09BlUw==", + "dependencies": { + "@swc/helpers": "^0.5.0" + } + }, + "node_modules/@internationalized/message": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@internationalized/message/-/message-3.1.4.tgz", + "integrity": "sha512-Dygi9hH1s7V9nha07pggCkvmRfDd3q2lWnMGvrJyrOwYMe1yj4D2T9BoH9I6MGR7xz0biQrtLPsqUkqXzIrBOw==", + "dependencies": { + "@swc/helpers": "^0.5.0", + "intl-messageformat": "^10.1.0" + } + }, + "node_modules/@internationalized/number": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/@internationalized/number/-/number-3.5.3.tgz", + "integrity": "sha512-rd1wA3ebzlp0Mehj5YTuTI50AQEx80gWFyHcQu+u91/5NgdwBecO8BH6ipPfE+lmQ9d63vpB3H9SHoIUiupllw==", + "dependencies": { + "@swc/helpers": "^0.5.0" + } + }, + "node_modules/@internationalized/string": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/@internationalized/string/-/string-3.2.3.tgz", + "integrity": "sha512-9kpfLoA8HegiWTeCbR2livhdVeKobCnVv8tlJ6M2jF+4tcMqDo94ezwlnrUANBWPgd8U7OXIHCk2Ov2qhk4KXw==", + "dependencies": { + "@swc/helpers": "^0.5.0" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", @@ -953,191 +1076,2590 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, + "node_modules/@launchpad-ui/alert": { + "version": "0.9.20", + "resolved": "https://registry.npmjs.org/@launchpad-ui/alert/-/alert-0.9.20.tgz", + "integrity": "sha512-zKBAis4Mzxtj8+dsQ/wrVmhkLjWZ/eiN9iVTBYmfI8iqlK9ITM4+m4YzBfQHkMowML+tE405eI6gPYWYz/d7OQ==", "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" + "@launchpad-ui/button": "~0.12.17", + "@launchpad-ui/icons": "~0.18.4", + "@launchpad-ui/tokens": "~0.9.12", + "@react-stately/utils": "3.10.1", + "classix": "2.1.17" }, - "engines": { - "node": ">= 8" + "peerDependencies": { + "react": "18.3.1", + "react-dom": "18.3.1" } }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, - "engines": { - "node": ">= 8" + "node_modules/@launchpad-ui/avatar": { + "version": "0.6.30", + "resolved": "https://registry.npmjs.org/@launchpad-ui/avatar/-/avatar-0.6.30.tgz", + "integrity": "sha512-b0IoCn0Jgq6DpxUI5lbiriyzxSKvFTd2b6/cx3mzD0y/6lgFBz3LaFNj6FXhb6b+qMchK2DkSfYKV+ErsqWa/w==", + "dependencies": { + "@launchpad-ui/icons": "~0.18.4", + "@launchpad-ui/tokens": "~0.9.12", + "classix": "2.1.17" + }, + "peerDependencies": { + "react": "18.3.1", + "react-dom": "18.3.1" } }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, + "node_modules/@launchpad-ui/banner": { + "version": "0.10.30", + "resolved": "https://registry.npmjs.org/@launchpad-ui/banner/-/banner-0.10.30.tgz", + "integrity": "sha512-b77fZA6es+lLMCoqKHidb3ayh3r9FsUo9Rq3ZZm4D4Wk8qxi2lwORU/5+R7rGWq5kTfaBRCNiDGbGWOGcQCZng==", "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" + "@launchpad-ui/button": "~0.12.17", + "@launchpad-ui/icons": "~0.18.4", + "@launchpad-ui/tokens": "~0.9.12", + "classix": "2.1.17" }, - "engines": { - "node": ">= 8" + "peerDependencies": { + "react": "18.3.1", + "react-dom": "18.3.1" } }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.18.0.tgz", - "integrity": "sha512-Tya6xypR10giZV1XzxmH5wr25VcZSncG0pZIjfePT0OVBvqNEurzValetGNarVrGiq66EBVAFn15iYX4w6FKgQ==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ] + "node_modules/@launchpad-ui/box": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/@launchpad-ui/box/-/box-0.1.13.tgz", + "integrity": "sha512-uoErz95ETafuMmAeSPVCMovT7gB7qyDWOkQB+kA8fQwtIl7/JxuawcsRl00cgVkyNv2mhs7o4PwIPhfAT3KDwg==", + "dependencies": { + "@launchpad-ui/tokens": "~0.9.12", + "@launchpad-ui/vars": "~0.2.19", + "@radix-ui/react-slot": "1.0.2", + "@vanilla-extract/dynamic": "2.1.0", + "rainbow-sprinkles": "0.17.0" + }, + "peerDependencies": { + "@vanilla-extract/css": "^1.14.0", + "react": "18.3.1", + "react-dom": "18.3.1" + } }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.18.0.tgz", - "integrity": "sha512-avCea0RAP03lTsDhEyfy+hpfr85KfyTctMADqHVhLAF3MlIkq83CP8UfAHUssgXTYd+6er6PaAhx/QGv4L1EiA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ] + "node_modules/@launchpad-ui/button": { + "version": "0.12.17", + "resolved": "https://registry.npmjs.org/@launchpad-ui/button/-/button-0.12.17.tgz", + "integrity": "sha512-6XT0wXZxZKnfowU/H+hrlxjFCoVCKKMqaQsnViABubphLQvo8KXX8xskzwA1GXGGBEh2U5pykkfPlmFCEC5vzw==", + "dependencies": { + "@launchpad-ui/icons": "~0.18.4", + "@launchpad-ui/tokens": "~0.9.12", + "@radix-ui/react-slot": "1.0.2", + "classix": "2.1.17" + }, + "peerDependencies": { + "react": "18.3.1", + "react-dom": "18.3.1" + } }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.18.0.tgz", - "integrity": "sha512-IWfdwU7KDSm07Ty0PuA/W2JYoZ4iTj3TUQjkVsO/6U+4I1jN5lcR71ZEvRh52sDOERdnNhhHU57UITXz5jC1/w==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ] + "node_modules/@launchpad-ui/card": { + "version": "0.2.35", + "resolved": "https://registry.npmjs.org/@launchpad-ui/card/-/card-0.2.35.tgz", + "integrity": "sha512-tMwD7ObfhS6YjbrboBep35nTzHogUeuNTI/27OtfdwmF0SRvuKmA/PaXAE1B5SSyepZmJMHKnzMn2suWfWarlA==", + "dependencies": { + "@launchpad-ui/form": "~0.11.20", + "@launchpad-ui/tokens": "~0.9.12", + "classix": "2.1.17" + }, + "peerDependencies": { + "react": "18.3.1", + "react-dom": "18.3.1" + } }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.18.0.tgz", - "integrity": "sha512-n2LMsUz7Ynu7DoQrSQkBf8iNrjOGyPLrdSg802vk6XT3FtsgX6JbE8IHRvposskFm9SNxzkLYGSq9QdpLYpRNA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ] + "node_modules/@launchpad-ui/chip": { + "version": "0.9.30", + "resolved": "https://registry.npmjs.org/@launchpad-ui/chip/-/chip-0.9.30.tgz", + "integrity": "sha512-+n/yYq3FU1JnFsKs5cYg0tsEBl7oU2a19iiiiTvDBXmcEV762plOwo/Vsh+9nbh5kbjUpwLypNgpsQqhz5wgCg==", + "dependencies": { + "@launchpad-ui/icons": "~0.18.4", + "@launchpad-ui/tokens": "~0.9.12", + "classix": "2.1.17" + }, + "peerDependencies": { + "react": "18.3.1", + "react-dom": "18.3.1" + } }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.18.0.tgz", - "integrity": "sha512-C/zbRYRXFjWvz9Z4haRxcTdnkPt1BtCkz+7RtBSuNmKzMzp3ZxdM28Mpccn6pt28/UWUCTXa+b0Mx1k3g6NOMA==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] + "node_modules/@launchpad-ui/clipboard": { + "version": "0.11.35", + "resolved": "https://registry.npmjs.org/@launchpad-ui/clipboard/-/clipboard-0.11.35.tgz", + "integrity": "sha512-ZXuRcFC3u00jbTAWhRmW6/rlcJYOXTXMCUtx/ytOpFdr7cD80gRuUFl/OyNgXJOOEnXP3ZT8TR4IHZWmlcrI9w==", + "dependencies": { + "@launchpad-ui/icons": "~0.18.4", + "@launchpad-ui/tokens": "~0.9.12", + "@launchpad-ui/tooltip": "~0.9.12", + "@radix-ui/react-slot": "1.0.2", + "@react-aria/live-announcer": "3.3.4", + "classix": "2.1.17" + }, + "peerDependencies": { + "react": "18.3.1", + "react-dom": "18.3.1" + } }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.18.0.tgz", - "integrity": "sha512-l3m9ewPgjQSXrUMHg93vt0hYCGnrMOcUpTz6FLtbwljo2HluS4zTXFy2571YQbisTnfTKPZ01u/ukJdQTLGh9A==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] + "node_modules/@launchpad-ui/collapsible": { + "version": "0.1.61", + "resolved": "https://registry.npmjs.org/@launchpad-ui/collapsible/-/collapsible-0.1.61.tgz", + "integrity": "sha512-13jplhIlUUxgaCeLCBAM/pkCYCNLEW1TaBwMHEVgelhcCu08JDU9VmSzONnJDa5KdzOWv7Ob64868R8gwwb+CA==", + "dependencies": { + "@launchpad-ui/button": "~0.12.17", + "@launchpad-ui/icons": "~0.18.4", + "@launchpad-ui/tokens": "~0.9.12", + "classix": "2.1.17" + }, + "peerDependencies": { + "react": "18.3.1", + "react-dom": "18.3.1" + } }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.18.0.tgz", - "integrity": "sha512-rJ5D47d8WD7J+7STKdCUAgmQk49xuFrRi9pZkWoRD1UeSMakbcepWXPF8ycChBoAqs1pb2wzvbY6Q33WmN2ftw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] + "node_modules/@launchpad-ui/columns": { + "version": "0.1.22", + "resolved": "https://registry.npmjs.org/@launchpad-ui/columns/-/columns-0.1.22.tgz", + "integrity": "sha512-HURsO+6Xk6QwnnLfFedXwAn38sdZ8xxFToXmbawtD07+uSnVDJULwDVAJytMD+NlXqFCsAYhjBZoBjrwUaRBCw==", + "dependencies": { + "@launchpad-ui/tokens": "~0.9.12", + "@launchpad-ui/types": "~0.1.1", + "classix": "2.1.17" + }, + "peerDependencies": { + "react": "18.3.1", + "react-dom": "18.3.1" + } }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.18.0.tgz", - "integrity": "sha512-be6Yx37b24ZwxQ+wOQXXLZqpq4jTckJhtGlWGZs68TgdKXJgw54lUUoFYrg6Zs/kjzAQwEwYbp8JxZVzZLRepQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] + "node_modules/@launchpad-ui/components": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@launchpad-ui/components/-/components-0.2.12.tgz", + "integrity": "sha512-bmhVFxcvmy+2F0dr04Qpfkp7mMNOp5AP/v7tftvTLLhfaya8Vh+75wCvsvgphoFQq0JSbf0KVfXOMQuxF5yGsQ==", + "dependencies": { + "@launchpad-ui/icons": "~0.18.4", + "@launchpad-ui/tokens": "~0.9.12", + "@react-aria/interactions": "3.21.3", + "@react-aria/toast": "3.0.0-beta.12", + "@react-aria/utils": "3.24.1", + "@react-stately/toast": "3.0.0-beta.4", + "@react-types/shared": "3.23.1", + "class-variance-authority": "0.7.0", + "react-aria": "3.33.1", + "react-aria-components": "1.2.1", + "react-router-dom": "6.16.0", + "react-stately": "3.31.1" + }, + "peerDependencies": { + "react": "18.3.1", + "react-dom": "18.3.1" + } }, - "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.18.0.tgz", - "integrity": "sha512-hNVMQK+qrA9Todu9+wqrXOHxFiD5YmdEi3paj6vP02Kx1hjd2LLYR2eaN7DsEshg09+9uzWi2W18MJDlG0cxJA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] + "node_modules/@launchpad-ui/core": { + "version": "0.49.22", + "resolved": "https://registry.npmjs.org/@launchpad-ui/core/-/core-0.49.22.tgz", + "integrity": "sha512-pEUVUMm6rlONqim6OqycA24zOzh19rGL0pk7EoHZfGfy9j4TCJIOnm6JjuPwDzEV1MsdUz7yXbNmSsePogFPqw==", + "dependencies": { + "@launchpad-ui/alert": "~0.9.20", + "@launchpad-ui/avatar": "~0.6.30", + "@launchpad-ui/banner": "~0.10.30", + "@launchpad-ui/box": "~0.1.13", + "@launchpad-ui/button": "~0.12.17", + "@launchpad-ui/card": "~0.2.35", + "@launchpad-ui/chip": "~0.9.30", + "@launchpad-ui/clipboard": "~0.11.35", + "@launchpad-ui/collapsible": "~0.1.61", + "@launchpad-ui/columns": "~0.1.22", + "@launchpad-ui/counter": "~0.4.16", + "@launchpad-ui/data-table": "~0.2.21", + "@launchpad-ui/drawer": "~0.5.35", + "@launchpad-ui/dropdown": "~0.6.109", + "@launchpad-ui/filter": "~0.7.20", + "@launchpad-ui/focus-trap": "~0.1.20", + "@launchpad-ui/form": "~0.11.20", + "@launchpad-ui/inline": "~0.1.22", + "@launchpad-ui/inline-edit": "~0.3.20", + "@launchpad-ui/markdown": "~0.5.18", + "@launchpad-ui/menu": "~0.13.20", + "@launchpad-ui/modal": "~0.17.35", + "@launchpad-ui/navigation": "~0.12.35", + "@launchpad-ui/overlay": "~0.3.30", + "@launchpad-ui/pagination": "~0.4.35", + "@launchpad-ui/popover": "~0.11.23", + "@launchpad-ui/portal": "~0.1.5", + "@launchpad-ui/progress": "~0.5.47", + "@launchpad-ui/progress-bubbles": "~0.7.23", + "@launchpad-ui/select": "~0.4.35", + "@launchpad-ui/slider": "~0.5.16", + "@launchpad-ui/snackbar": "~0.5.18", + "@launchpad-ui/split-button": "~0.10.20", + "@launchpad-ui/stack": "~0.1.22", + "@launchpad-ui/tab-list": "~0.5.22", + "@launchpad-ui/table": "~0.6.17", + "@launchpad-ui/tag": "~0.3.35", + "@launchpad-ui/toast": "~0.3.31", + "@launchpad-ui/toggle": "~0.7.22", + "@launchpad-ui/tooltip": "~0.9.12", + "@launchpad-ui/types": "~0.1.1" + }, + "peerDependencies": { + "react": "18.3.1", + "react-dom": "18.3.1" + } }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.18.0.tgz", - "integrity": "sha512-ROCM7i+m1NfdrsmvwSzoxp9HFtmKGHEqu5NNDiZWQtXLA8S5HBCkVvKAxJ8U+CVctHwV2Gb5VUaK7UAkzhDjlg==", - "cpu": [ - "riscv64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] + "node_modules/@launchpad-ui/counter": { + "version": "0.4.16", + "resolved": "https://registry.npmjs.org/@launchpad-ui/counter/-/counter-0.4.16.tgz", + "integrity": "sha512-LH2uN4AV+cAHdVJuS2D3Rg/IJP706cw1nWXr0VPPW8ihW1fa1U/klUmmiSXKtvtjmcbdLyGB6Y7dKKithyAoUw==", + "dependencies": { + "@launchpad-ui/tokens": "~0.9.12", + "classix": "2.1.17" + }, + "peerDependencies": { + "react": "18.3.1", + "react-dom": "18.3.1" + } }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.18.0.tgz", - "integrity": "sha512-0UyyRHyDN42QL+NbqevXIIUnKA47A+45WyasO+y2bGJ1mhQrfrtXUpTxCOrfxCR4esV3/RLYyucGVPiUsO8xjg==", - "cpu": [ - "s390x" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] + "node_modules/@launchpad-ui/data-table": { + "version": "0.2.21", + "resolved": "https://registry.npmjs.org/@launchpad-ui/data-table/-/data-table-0.2.21.tgz", + "integrity": "sha512-GEqR07JKgC18O2YYwyxKl7SNLgkI6KCbcnGa0eHP/6YAZSAZFwPJ289JEfyKZTLKEosgbplQV1SGNglKOWaLRQ==", + "dependencies": { + "@launchpad-ui/tokens": "~0.9.12", + "@launchpad-ui/vars": "~0.2.19", + "@react-aria/checkbox": "3.14.3", + "@react-aria/focus": "3.17.1", + "@react-aria/table": "3.14.1", + "@react-aria/utils": "3.24.1", + "@react-aria/visually-hidden": "3.8.12", + "@react-stately/table": "3.11.8", + "@react-stately/toggle": "3.7.4", + "@react-types/grid": "3.2.6", + "@vanilla-extract/recipes": "^0.5.0", + "classix": "2.1.17" + }, + "peerDependencies": { + "@react-stately/table": "3.11.8", + "@vanilla-extract/css": "^1.14.0", + "react": "18.3.1", + "react-dom": "18.3.1" + } }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.18.0.tgz", - "integrity": "sha512-xuglR2rBVHA5UsI8h8UbX4VJ470PtGCf5Vpswh7p2ukaqBGFTnsfzxUBetoWBWymHMxbIG0Cmx7Y9qDZzr648w==", - "cpu": [ - "x64" - ], + "node_modules/@launchpad-ui/drawer": { + "version": "0.5.35", + "resolved": "https://registry.npmjs.org/@launchpad-ui/drawer/-/drawer-0.5.35.tgz", + "integrity": "sha512-IlKpK38YHIJJPJSZSyX7W5GoixukOzYk0BRoYcG18GoYS5orf8w4yCBWx93xh56/s71ZyMaJSlYOmv0eh3WUEg==", + "dependencies": { + "@launchpad-ui/button": "~0.12.17", + "@launchpad-ui/focus-trap": "~0.1.20", + "@launchpad-ui/icons": "~0.18.4", + "@launchpad-ui/portal": "~0.1.5", + "@launchpad-ui/progress": "~0.5.47", + "@launchpad-ui/tokens": "~0.9.12", + "@react-aria/interactions": "3.21.3", + "@react-aria/overlays": "3.22.1", + "classix": "2.1.17", + "framer-motion": "11.2.4" + }, + "peerDependencies": { + "react": "18.3.1", + "react-dom": "18.3.1" + } + }, + "node_modules/@launchpad-ui/dropdown": { + "version": "0.6.109", + "resolved": "https://registry.npmjs.org/@launchpad-ui/dropdown/-/dropdown-0.6.109.tgz", + "integrity": "sha512-vKwDHSniDZCyK4NZo7Q8GhxK56HexSnFb7dc5075K6zgIDNkVMyY9WiUbD/PYkOIVWuKWn+2F+4tla6fIHQ5dA==", + "dependencies": { + "@launchpad-ui/button": "~0.12.17", + "@launchpad-ui/icons": "~0.18.4", + "@launchpad-ui/popover": "~0.11.23", + "@launchpad-ui/tokens": "~0.9.12", + "@react-aria/utils": "3.24.1", + "classix": "2.1.17" + }, + "peerDependencies": { + "react": "18.3.1", + "react-dom": "18.3.1" + } + }, + "node_modules/@launchpad-ui/filter": { + "version": "0.7.20", + "resolved": "https://registry.npmjs.org/@launchpad-ui/filter/-/filter-0.7.20.tgz", + "integrity": "sha512-8+KcL5xZKH7sYsyj8Tz2pr2QtMA1AjDF5bv/JkIKWgFLXsUdzvXk2JoTKDRq3wI72T3A9UGiYI4zxX7+5VpFfw==", + "dependencies": { + "@launchpad-ui/button": "~0.12.17", + "@launchpad-ui/dropdown": "~0.6.109", + "@launchpad-ui/icons": "~0.18.4", + "@launchpad-ui/menu": "~0.13.20", + "@launchpad-ui/tokens": "~0.9.12", + "@launchpad-ui/tooltip": "~0.9.12", + "@react-aria/visually-hidden": "3.8.12", + "classix": "2.1.17" + }, + "peerDependencies": { + "react": "18.3.1", + "react-dom": "18.3.1" + } + }, + "node_modules/@launchpad-ui/focus-trap": { + "version": "0.1.20", + "resolved": "https://registry.npmjs.org/@launchpad-ui/focus-trap/-/focus-trap-0.1.20.tgz", + "integrity": "sha512-n/nHfKaj5G8rxw9ok10llFff4QmyUPUhMlISBuh7ZuMW0GeBD5EqLiE64IyXDCSAFJqUt6nvx+peN9tHyqatsQ==", + "dependencies": { + "@react-aria/focus": "3.17.1" + }, + "peerDependencies": { + "react": "18.3.1", + "react-dom": "18.3.1" + } + }, + "node_modules/@launchpad-ui/form": { + "version": "0.11.20", + "resolved": "https://registry.npmjs.org/@launchpad-ui/form/-/form-0.11.20.tgz", + "integrity": "sha512-X6Gx2p3wWC8wdZMYV7VM8K0Q9BMRi5jP8/RDR2GP9NyQ+4It6XXwL2vTmlaOQhdiJfILDlaSQqTYxYZNF342Pw==", + "dependencies": { + "@launchpad-ui/button": "~0.12.17", + "@launchpad-ui/icons": "~0.18.4", + "@launchpad-ui/tokens": "~0.9.12", + "@launchpad-ui/tooltip": "~0.9.12", + "@react-aria/button": "3.9.5", + "@react-aria/i18n": "3.11.1", + "@react-aria/numberfield": "3.11.3", + "@react-aria/visually-hidden": "3.8.12", + "@react-stately/numberfield": "3.9.3", + "classix": "2.1.17" + }, + "peerDependencies": { + "react": "18.3.1", + "react-dom": "18.3.1" + } + }, + "node_modules/@launchpad-ui/icons": { + "version": "0.18.4", + "resolved": "https://registry.npmjs.org/@launchpad-ui/icons/-/icons-0.18.4.tgz", + "integrity": "sha512-hcHUJFcxxPRs8PIWXZHAQGmezecEW+SUAoJ/FGAjtZFxse+iHD/IxMyzNIvNiWOqTFjew7Jt16U9veHvJfth4A==", + "dependencies": { + "@launchpad-ui/tokens": "~0.9.12", + "classix": "2.1.17" + }, + "peerDependencies": { + "react": "18.3.1", + "react-dom": "18.3.1" + } + }, + "node_modules/@launchpad-ui/inline": { + "version": "0.1.22", + "resolved": "https://registry.npmjs.org/@launchpad-ui/inline/-/inline-0.1.22.tgz", + "integrity": "sha512-v0gdWJzgQhWbLpo45Gw9DiT0bK5OEmb6pf6zpIu1mavvfpgqRoI724DIKcgXJQ7zoP+Hna3YtCY8HZqGMfqpEw==", + "dependencies": { + "@launchpad-ui/tokens": "~0.9.12", + "@launchpad-ui/types": "~0.1.1", + "classix": "2.1.17" + }, + "peerDependencies": { + "react": "18.3.1", + "react-dom": "18.3.1" + } + }, + "node_modules/@launchpad-ui/inline-edit": { + "version": "0.3.20", + "resolved": "https://registry.npmjs.org/@launchpad-ui/inline-edit/-/inline-edit-0.3.20.tgz", + "integrity": "sha512-TGyld7YP3I9xs/t4kR6xof/2QGHxX/jDJ/SnhbCYeiHW94AADT7MKwO60tcZElTBVRai6oSo4C2v6kqzgl7AQg==", + "dependencies": { + "@launchpad-ui/button": "~0.12.17", + "@launchpad-ui/form": "~0.11.20", + "@launchpad-ui/icons": "~0.18.4", + "@launchpad-ui/tokens": "~0.9.12", + "@launchpad-ui/vars": "~0.2.19", + "@react-aria/button": "3.9.5", + "@react-aria/focus": "3.17.1", + "@react-aria/interactions": "3.21.3", + "@react-aria/utils": "3.24.1", + "@vanilla-extract/recipes": "^0.5.0", + "classix": "2.1.17" + }, + "peerDependencies": { + "@vanilla-extract/css": "^1.14.0", + "react": "18.3.1", + "react-dom": "18.3.1" + } + }, + "node_modules/@launchpad-ui/markdown": { + "version": "0.5.18", + "resolved": "https://registry.npmjs.org/@launchpad-ui/markdown/-/markdown-0.5.18.tgz", + "integrity": "sha512-RT3Aeb1+7zlYMg+llp0aFv0ZGrm94maZ6KwQ2Tvl1jMpByqOJdOm6qkohOr713rNO2zykoZG2slCFICw9qbuhw==", + "dependencies": { + "@launchpad-ui/tokens": "~0.9.12", + "classix": "2.1.17", + "isomorphic-dompurify": "^2.12.0", + "marked": "^13.0.0" + }, + "peerDependencies": { + "react": "18.3.1", + "react-dom": "18.3.1" + } + }, + "node_modules/@launchpad-ui/menu": { + "version": "0.13.20", + "resolved": "https://registry.npmjs.org/@launchpad-ui/menu/-/menu-0.13.20.tgz", + "integrity": "sha512-TmJQvN+6aEiCo96AAF7BJ1sprF8bNLMizyjr9ENOs9qVoe0zp37QmBVkSCq136Ccrohp5hCd7+X3ITxBuNbPeQ==", + "dependencies": { + "@launchpad-ui/form": "~0.11.20", + "@launchpad-ui/icons": "~0.18.4", + "@launchpad-ui/popover": "~0.11.23", + "@launchpad-ui/tokens": "~0.9.12", + "@launchpad-ui/tooltip": "~0.9.12", + "@radix-ui/react-slot": "1.0.2", + "@react-aria/focus": "3.17.1", + "@react-aria/separator": "3.3.13", + "classix": "2.1.17", + "react-virtual": "2.10.4" + }, + "peerDependencies": { + "react": "18.3.1", + "react-dom": "18.3.1" + } + }, + "node_modules/@launchpad-ui/modal": { + "version": "0.17.35", + "resolved": "https://registry.npmjs.org/@launchpad-ui/modal/-/modal-0.17.35.tgz", + "integrity": "sha512-XAcvaHN4TSPL+OSiTRlwEOK7F7fpiudRPPOtri77bRqW6RRqAHlrHJknYUTbxmj6ZixD9zQncFURG2Gb9jLw2A==", + "dependencies": { + "@launchpad-ui/button": "~0.12.17", + "@launchpad-ui/focus-trap": "~0.1.20", + "@launchpad-ui/icons": "~0.18.4", + "@launchpad-ui/portal": "~0.1.5", + "@launchpad-ui/tokens": "~0.9.12", + "@react-aria/interactions": "3.21.3", + "@react-aria/overlays": "3.22.1", + "classix": "2.1.17", + "framer-motion": "11.2.4" + }, + "peerDependencies": { + "react": "18.3.1", + "react-dom": "18.3.1" + } + }, + "node_modules/@launchpad-ui/navigation": { + "version": "0.12.35", + "resolved": "https://registry.npmjs.org/@launchpad-ui/navigation/-/navigation-0.12.35.tgz", + "integrity": "sha512-on4tScWncd+YMqKJAGhkaO06tgaNEhvAbDolBw8Kiiimwur5dN16BjGcWjEmfNMV5sd9wl8bZGBmQXoigBDV1g==", + "dependencies": { + "@launchpad-ui/chip": "~0.9.30", + "@launchpad-ui/dropdown": "~0.6.109", + "@launchpad-ui/icons": "~0.18.4", + "@launchpad-ui/menu": "~0.13.20", + "@launchpad-ui/popover": "~0.11.23", + "@launchpad-ui/tokens": "~0.9.12", + "@launchpad-ui/tooltip": "~0.9.12", + "@react-aria/utils": "3.24.1", + "@react-stately/list": "3.10.5", + "@react-types/shared": "3.23.1", + "classix": "2.1.17", + "react-router-dom": "6.16.0" + }, + "peerDependencies": { + "react": "18.3.1", + "react-dom": "18.3.1" + } + }, + "node_modules/@launchpad-ui/overlay": { + "version": "0.3.30", + "resolved": "https://registry.npmjs.org/@launchpad-ui/overlay/-/overlay-0.3.30.tgz", + "integrity": "sha512-KLQez0pA0HaF0Anse8MRsh+T6ruIswXC0ltQyFQS9USnOLTCqtjSYqXdgLx9O1ecbGidQT1CPHvEIRsORUxEvg==", + "dependencies": { + "@launchpad-ui/portal": "~0.1.5" + }, + "peerDependencies": { + "react": "18.2.0", + "react-dom": "18.2.0" + } + }, + "node_modules/@launchpad-ui/pagination": { + "version": "0.4.35", + "resolved": "https://registry.npmjs.org/@launchpad-ui/pagination/-/pagination-0.4.35.tgz", + "integrity": "sha512-F+VQDXJ4pZnp99ZbJ6A0SrJX+WJ4RJHHqihZcX9vB+NVl3XZgjYE2+W/UWM/dK9KDMmsokGv9B+qV1FDNyKF+w==", + "dependencies": { + "@launchpad-ui/button": "~0.12.17", + "@launchpad-ui/icons": "~0.18.4", + "@launchpad-ui/progress": "~0.5.47", + "@launchpad-ui/tokens": "~0.9.12", + "@react-aria/visually-hidden": "3.8.12", + "classix": "2.1.17" + }, + "peerDependencies": { + "react": "18.3.1", + "react-dom": "18.3.1" + } + }, + "node_modules/@launchpad-ui/popover": { + "version": "0.11.23", + "resolved": "https://registry.npmjs.org/@launchpad-ui/popover/-/popover-0.11.23.tgz", + "integrity": "sha512-x6B+/cZm5NcJXwLMSBXK+xeg/Yfk12AjgNmhU3IDFC9XjDVaNNKIheQe/oFiz2/29FLkhYmU/numSzDojxexfg==", + "dependencies": { + "@floating-ui/core": "1.6.0", + "@floating-ui/dom": "1.6.1", + "@launchpad-ui/focus-trap": "~0.1.20", + "@launchpad-ui/overlay": "~0.3.30", + "@launchpad-ui/tokens": "~0.9.12", + "classix": "2.1.17", + "framer-motion": "11.2.4" + }, + "peerDependencies": { + "react": "18.3.1", + "react-dom": "18.3.1" + } + }, + "node_modules/@launchpad-ui/portal": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/@launchpad-ui/portal/-/portal-0.1.5.tgz", + "integrity": "sha512-swNZFu99gVz0BdOS95EFdZVNU7KIgrxiZtTFNNJrKLMp4QzsvhJt6mB8cPArPXM5UH8kHLVTDXCMc9oZg66m+A==", + "peerDependencies": { + "react": "18.2.0", + "react-dom": "18.2.0" + } + }, + "node_modules/@launchpad-ui/progress": { + "version": "0.5.47", + "resolved": "https://registry.npmjs.org/@launchpad-ui/progress/-/progress-0.5.47.tgz", + "integrity": "sha512-Ai1q0lQ2zOSdvn7bWePSzWy6aGn7xArkkq+osp0PFAf3gn+E2uw0VHWYUnf68XEWbDjZL60L+LxHv56AHBGd9w==", + "dependencies": { + "@launchpad-ui/tokens": "~0.9.12", + "classix": "2.1.17" + }, + "peerDependencies": { + "react": "18.3.1", + "react-dom": "18.3.1" + } + }, + "node_modules/@launchpad-ui/progress-bubbles": { + "version": "0.7.23", + "resolved": "https://registry.npmjs.org/@launchpad-ui/progress-bubbles/-/progress-bubbles-0.7.23.tgz", + "integrity": "sha512-vIBJBGmUFEBLusHzJmqPH+iT3MWZnoCPLGEsrE7WmwYQ2sQWJgAejKJAai3ulU1LOZ3TzL7JTUjiBQlczMERYw==", + "dependencies": { + "@launchpad-ui/popover": "~0.11.23", + "@launchpad-ui/tokens": "~0.9.12", + "classix": "2.1.17" + }, + "peerDependencies": { + "react": "18.3.1", + "react-dom": "18.3.1" + } + }, + "node_modules/@launchpad-ui/select": { + "version": "0.4.35", + "resolved": "https://registry.npmjs.org/@launchpad-ui/select/-/select-0.4.35.tgz", + "integrity": "sha512-U0VypPwXHghj/Ajew/Y+QpZjEXLdvotWcXrPqLrOTNE+QBNgkDqB34P29NUTd7MqBUlN/VzeNiX10RnVHlFY5A==", + "dependencies": { + "@launchpad-ui/button": "~0.12.17", + "@launchpad-ui/icons": "~0.18.4", + "@launchpad-ui/modal": "~0.17.35", + "@launchpad-ui/popover": "~0.11.23", + "@launchpad-ui/tokens": "~0.9.12", + "@launchpad-ui/tooltip": "~0.9.12", + "@react-aria/button": "3.9.5", + "@react-aria/combobox": "3.9.1", + "@react-aria/focus": "3.17.1", + "@react-aria/gridlist": "3.8.1", + "@react-aria/interactions": "3.21.3", + "@react-aria/label": "3.7.8", + "@react-aria/listbox": "3.12.1", + "@react-aria/menu": "3.14.1", + "@react-aria/overlays": "3.22.1", + "@react-aria/select": "3.14.5", + "@react-aria/selection": "3.18.1", + "@react-aria/separator": "3.3.13", + "@react-aria/textfield": "3.14.5", + "@react-aria/utils": "3.24.1", + "@react-aria/visually-hidden": "3.8.12", + "@react-stately/collections": "3.10.7", + "@react-stately/combobox": "3.8.4", + "@react-stately/data": "3.11.4", + "@react-stately/list": "3.10.5", + "@react-stately/menu": "3.7.1", + "@react-stately/overlays": "3.6.7", + "@react-stately/select": "3.6.4", + "@react-stately/selection": "3.15.1", + "@react-stately/utils": "3.10.1", + "@react-types/button": "3.9.4", + "@react-types/combobox": "3.11.1", + "@react-types/overlays": "3.8.7", + "@react-types/select": "3.9.4", + "@react-types/shared": "3.23.1", + "classix": "2.1.17" + }, + "peerDependencies": { + "react": "18.3.1", + "react-dom": "18.3.1" + } + }, + "node_modules/@launchpad-ui/slider": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/@launchpad-ui/slider/-/slider-0.5.16.tgz", + "integrity": "sha512-Qt5349CLr7M6ESEWwhesiVW4YHTHE37YhiFoGuIAJubw3xd44WcCSSM+54Y+pQT8P1ZxnZoQ+p9Nvrc7clGlGg==", + "dependencies": { + "@launchpad-ui/tokens": "~0.9.12", + "classix": "2.1.17" + }, + "peerDependencies": { + "react": "18.3.1", + "react-dom": "18.3.1" + } + }, + "node_modules/@launchpad-ui/snackbar": { + "version": "0.5.18", + "resolved": "https://registry.npmjs.org/@launchpad-ui/snackbar/-/snackbar-0.5.18.tgz", + "integrity": "sha512-Od+G4NCSw5mBqwTAaAOzzcpdgQjU8Id96tXCictGxRyJKb+oH3GExpcM0EAgDlPH1zBktIICcr8GN4YgsWZuxg==", + "dependencies": { + "@launchpad-ui/button": "~0.12.17", + "@launchpad-ui/icons": "~0.18.4", + "@launchpad-ui/tokens": "~0.9.12", + "classix": "2.1.17", + "framer-motion": "11.2.4" + }, + "peerDependencies": { + "react": "18.3.1", + "react-dom": "18.3.1" + } + }, + "node_modules/@launchpad-ui/split-button": { + "version": "0.10.20", + "resolved": "https://registry.npmjs.org/@launchpad-ui/split-button/-/split-button-0.10.20.tgz", + "integrity": "sha512-ZILhoKCaLfA1ZDF5fzL5N5nyBMk3BTD4FVU0p4RgJM9q8zYUU4HJBZ3Tasna407YxrsCZQTFV/jJry5KBJ7c6A==", + "dependencies": { + "@launchpad-ui/button": "~0.12.17", + "@launchpad-ui/dropdown": "~0.6.109", + "@launchpad-ui/popover": "~0.11.23", + "@launchpad-ui/tokens": "~0.9.12", + "@launchpad-ui/tooltip": "~0.9.12", + "classix": "2.1.17" + }, + "peerDependencies": { + "react": "18.3.1", + "react-dom": "18.3.1" + } + }, + "node_modules/@launchpad-ui/stack": { + "version": "0.1.22", + "resolved": "https://registry.npmjs.org/@launchpad-ui/stack/-/stack-0.1.22.tgz", + "integrity": "sha512-3oQWEe+wmhiF6vy6ZgGaVNIcbiwSP4yJurhGSEYMNbxwK7XhnjZyziUucBBg+55z8M9nHYP7mS1t50k2hjSKKw==", + "dependencies": { + "@launchpad-ui/tokens": "~0.9.12", + "@launchpad-ui/types": "~0.1.1", + "classix": "2.1.17" + }, + "peerDependencies": { + "react": "18.3.1", + "react-dom": "18.3.1" + } + }, + "node_modules/@launchpad-ui/tab-list": { + "version": "0.5.22", + "resolved": "https://registry.npmjs.org/@launchpad-ui/tab-list/-/tab-list-0.5.22.tgz", + "integrity": "sha512-k/8SpOOuqpNSGsIblwB8S1IaJfQUSBkqjz4osl5m9AE4+E4DGg+U0+QY8ssm/PjPMEaaOh/dn1SE5KufWo94Lw==", + "dependencies": { + "@launchpad-ui/tokens": "~0.9.12", + "@react-aria/tabs": "3.9.1", + "@react-stately/tabs": "3.6.6", + "@react-types/shared": "3.23.1", + "@react-types/tabs": "3.3.7", + "classix": "2.1.17" + }, + "peerDependencies": { + "@react-stately/collections": "3.10.7", + "react": "18.3.1", + "react-dom": "18.3.1" + } + }, + "node_modules/@launchpad-ui/table": { + "version": "0.6.17", + "resolved": "https://registry.npmjs.org/@launchpad-ui/table/-/table-0.6.17.tgz", + "integrity": "sha512-rOcHHgLMVGFds8aa69UDOd3mcibvVZfAIvJ8F1AYUNVc6Ynb7uUzGkjEjySbaDakbQ0zcyjYMozVcZ4WBlDVAg==", + "dependencies": { + "@launchpad-ui/tokens": "~0.9.12", + "classix": "2.1.17" + }, + "peerDependencies": { + "react": "18.3.1", + "react-dom": "18.3.1" + } + }, + "node_modules/@launchpad-ui/tag": { + "version": "0.3.35", + "resolved": "https://registry.npmjs.org/@launchpad-ui/tag/-/tag-0.3.35.tgz", + "integrity": "sha512-RRwiaF158Wcu4b+T6Aaefzr5Yn7SK4Mg7j0bPZ7XoboClzbzranxaTm2IwC3YOPyf1GTmJ8+3bT6dRPrgBxsQw==", + "dependencies": { + "@launchpad-ui/button": "~0.12.17", + "@launchpad-ui/icons": "~0.18.4", + "@launchpad-ui/tokens": "~0.9.12", + "@launchpad-ui/tooltip": "~0.9.12", + "@react-aria/focus": "3.17.1", + "@react-aria/interactions": "3.21.3", + "@react-aria/selection": "3.18.1", + "@react-aria/tag": "3.4.1", + "@react-aria/utils": "3.24.1", + "@react-stately/list": "3.10.5", + "@react-types/shared": "3.23.1", + "classix": "2.1.17" + }, + "peerDependencies": { + "react": "18.3.1", + "react-dom": "18.3.1" + } + }, + "node_modules/@launchpad-ui/toast": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@launchpad-ui/toast/-/toast-0.3.31.tgz", + "integrity": "sha512-TdFFK2sw0xqTD1olN/zw6gh09q3s92zi0v98ub3wISfZtibGf4EPjMIcx0bFbdYC13kBMoemoVRbpew2cb5Pvw==", + "dependencies": { + "@launchpad-ui/icons": "~0.18.4", + "@launchpad-ui/tokens": "~0.9.12", + "classix": "2.1.17", + "framer-motion": "11.2.4" + }, + "peerDependencies": { + "react": "18.3.1", + "react-dom": "18.3.1" + } + }, + "node_modules/@launchpad-ui/toggle": { + "version": "0.7.22", + "resolved": "https://registry.npmjs.org/@launchpad-ui/toggle/-/toggle-0.7.22.tgz", + "integrity": "sha512-X3FXxdM1a32x1qbcCLQT3C6IdBwIkt07SfRjlbYql1socANcCmWOdRD1Qd0JWjARpoDMDediGZtJZ6EsLUxroQ==", + "dependencies": { + "@launchpad-ui/tokens": "~0.9.12", + "@react-aria/focus": "3.17.1", + "@react-aria/switch": "3.6.4", + "@react-stately/toggle": "3.7.4", + "classix": "2.1.17" + }, + "peerDependencies": { + "react": "18.3.1", + "react-dom": "18.3.1" + } + }, + "node_modules/@launchpad-ui/tokens": { + "version": "0.9.12", + "resolved": "https://registry.npmjs.org/@launchpad-ui/tokens/-/tokens-0.9.12.tgz", + "integrity": "sha512-FEDBt0s+b0hlwkhSU/TiY5YXseLQYGKeIgqZUngmUiPTgaSvttlBm4BSVtoly3JK00ihNGZsvEtZVoUgp+zoKA==" + }, + "node_modules/@launchpad-ui/tooltip": { + "version": "0.9.12", + "resolved": "https://registry.npmjs.org/@launchpad-ui/tooltip/-/tooltip-0.9.12.tgz", + "integrity": "sha512-qsEnQAXOEpPyyoMEN0fa1D8mgKYPMd09VGQCg65RS0KNNbK/TjewbvtKi07AZdzol1hTzArnpCaM7/INPRLBoA==", + "dependencies": { + "@launchpad-ui/popover": "~0.11.23", + "@launchpad-ui/tokens": "~0.9.12", + "classix": "2.1.17" + }, + "peerDependencies": { + "react": "18.3.1", + "react-dom": "18.3.1" + } + }, + "node_modules/@launchpad-ui/types": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@launchpad-ui/types/-/types-0.1.1.tgz", + "integrity": "sha512-0mHnqVNIHNwrRe4uw5DacWp1N+WNFt6PgVs/5xAuO6tlH5WXHq1WqEg3H4rKi46sXwsepEaijdhKQC3xlajcmA==" + }, + "node_modules/@launchpad-ui/vars": { + "version": "0.2.19", + "resolved": "https://registry.npmjs.org/@launchpad-ui/vars/-/vars-0.2.19.tgz", + "integrity": "sha512-fvSRN/mQ+DpuaLpXO4usLaqfaqb0u0M6ryMLIMpqK9kdv5gTjbha3GWOv0MVz5PKDBWFkXdgNtvvkWzYjlgZQA==", + "dependencies": { + "@launchpad-ui/tokens": "~0.9.12" + }, + "peerDependencies": { + "@vanilla-extract/css": "^1.14.0" + }, + "peerDependenciesMeta": { + "@vanilla-extract/css": { + "optional": true + } + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.1.tgz", + "integrity": "sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.2.tgz", + "integrity": "sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@reach/observe-rect": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@reach/observe-rect/-/observe-rect-1.2.0.tgz", + "integrity": "sha512-Ba7HmkFgfQxZqqaeIWWkNK0rEhpxVQHIoVyW1YDSkGsGIXzcaW4deC8B0pZrNSSyLTdIk7y+5olKt5+g0GmFIQ==" + }, + "node_modules/@react-aria/breadcrumbs": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@react-aria/breadcrumbs/-/breadcrumbs-3.5.13.tgz", + "integrity": "sha512-G1Gqf/P6kVdfs94ovwP18fTWuIxadIQgHsXS08JEVcFVYMjb9YjqnEBaohUxD1tq2WldMbYw53ahQblT4NTG+g==", + "dependencies": { + "@react-aria/i18n": "^3.11.1", + "@react-aria/link": "^3.7.1", + "@react-aria/utils": "^3.24.1", + "@react-types/breadcrumbs": "^3.7.5", + "@react-types/shared": "^3.23.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@react-aria/button": { + "version": "3.9.5", + "resolved": "https://registry.npmjs.org/@react-aria/button/-/button-3.9.5.tgz", + "integrity": "sha512-dgcYR6j8WDOMLKuVrtxzx4jIC05cVKDzc+HnPO8lNkBAOfjcuN5tkGRtIjLtqjMvpZHhQT5aDbgFpIaZzxgFIg==", + "dependencies": { + "@react-aria/focus": "^3.17.1", + "@react-aria/interactions": "^3.21.3", + "@react-aria/utils": "^3.24.1", + "@react-stately/toggle": "^3.7.4", + "@react-types/button": "^3.9.4", + "@react-types/shared": "^3.23.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@react-aria/calendar": { + "version": "3.5.8", + "resolved": "https://registry.npmjs.org/@react-aria/calendar/-/calendar-3.5.8.tgz", + "integrity": "sha512-Whlp4CeAA5/ZkzrAHUv73kgIRYjw088eYGSc+cvSOCxfrc/2XkBm9rNrnSBv0DvhJ8AG0Fjz3vYakTmF3BgZBw==", + "dependencies": { + "@internationalized/date": "^3.5.4", + "@react-aria/i18n": "^3.11.1", + "@react-aria/interactions": "^3.21.3", + "@react-aria/live-announcer": "^3.3.4", + "@react-aria/utils": "^3.24.1", + "@react-stately/calendar": "^3.5.1", + "@react-types/button": "^3.9.4", + "@react-types/calendar": "^3.4.6", + "@react-types/shared": "^3.23.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@react-aria/checkbox": { + "version": "3.14.3", + "resolved": "https://registry.npmjs.org/@react-aria/checkbox/-/checkbox-3.14.3.tgz", + "integrity": "sha512-EtBJL6iu0gvrw3A4R7UeVLR6diaVk/mh4kFBc7c8hQjpEJweRr4hmJT3hrNg3MBcTWLxFiMEXPGgWEwXDBygtA==", + "dependencies": { + "@react-aria/form": "^3.0.5", + "@react-aria/interactions": "^3.21.3", + "@react-aria/label": "^3.7.8", + "@react-aria/toggle": "^3.10.4", + "@react-aria/utils": "^3.24.1", + "@react-stately/checkbox": "^3.6.5", + "@react-stately/form": "^3.0.3", + "@react-stately/toggle": "^3.7.4", + "@react-types/checkbox": "^3.8.1", + "@react-types/shared": "^3.23.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@react-aria/color": { + "version": "3.0.0-beta.33", + "resolved": "https://registry.npmjs.org/@react-aria/color/-/color-3.0.0-beta.33.tgz", + "integrity": "sha512-nhqnIHYm5p6MbuF3cC6lnqzG7MjwBsBd0DtpO+ByFYO+zxpMMbeC5R+1SFxvapR4uqmAzTotbtiUCGsG+SUaIg==", + "dependencies": { + "@react-aria/i18n": "^3.11.1", + "@react-aria/interactions": "^3.21.3", + "@react-aria/numberfield": "^3.11.3", + "@react-aria/slider": "^3.7.8", + "@react-aria/spinbutton": "^3.6.5", + "@react-aria/textfield": "^3.14.5", + "@react-aria/utils": "^3.24.1", + "@react-aria/visually-hidden": "^3.8.12", + "@react-stately/color": "^3.6.1", + "@react-stately/form": "^3.0.3", + "@react-types/color": "3.0.0-beta.25", + "@react-types/shared": "^3.23.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@react-aria/combobox": { + "version": "3.9.1", + "resolved": "https://registry.npmjs.org/@react-aria/combobox/-/combobox-3.9.1.tgz", + "integrity": "sha512-SpK92dCmT8qn8aEcUAihRQrBb5LZUhwIbDExFII8PvUvEFy/PoQHXIo3j1V29WkutDBDpMvBv/6XRCHGXPqrhQ==", + "dependencies": { + "@react-aria/i18n": "^3.11.1", + "@react-aria/listbox": "^3.12.1", + "@react-aria/live-announcer": "^3.3.4", + "@react-aria/menu": "^3.14.1", + "@react-aria/overlays": "^3.22.1", + "@react-aria/selection": "^3.18.1", + "@react-aria/textfield": "^3.14.5", + "@react-aria/utils": "^3.24.1", + "@react-stately/collections": "^3.10.7", + "@react-stately/combobox": "^3.8.4", + "@react-stately/form": "^3.0.3", + "@react-types/button": "^3.9.4", + "@react-types/combobox": "^3.11.1", + "@react-types/shared": "^3.23.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@react-aria/datepicker": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/@react-aria/datepicker/-/datepicker-3.10.1.tgz", + "integrity": "sha512-4HZL593nrNMa1GjBmWEN/OTvNS6d3/16G1YJWlqiUlv11ADulSbqBIjMmkgwrJVFcjrgqtXFy+yyrTA/oq94Zw==", + "dependencies": { + "@internationalized/date": "^3.5.4", + "@internationalized/number": "^3.5.3", + "@internationalized/string": "^3.2.3", + "@react-aria/focus": "^3.17.1", + "@react-aria/form": "^3.0.5", + "@react-aria/i18n": "^3.11.1", + "@react-aria/interactions": "^3.21.3", + "@react-aria/label": "^3.7.8", + "@react-aria/spinbutton": "^3.6.5", + "@react-aria/utils": "^3.24.1", + "@react-stately/datepicker": "^3.9.4", + "@react-stately/form": "^3.0.3", + "@react-types/button": "^3.9.4", + "@react-types/calendar": "^3.4.6", + "@react-types/datepicker": "^3.7.4", + "@react-types/dialog": "^3.5.10", + "@react-types/shared": "^3.23.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@react-aria/dialog": { + "version": "3.5.14", + "resolved": "https://registry.npmjs.org/@react-aria/dialog/-/dialog-3.5.14.tgz", + "integrity": "sha512-oqDCjQ8hxe3GStf48XWBf2CliEnxlR9GgSYPHJPUc69WBj68D9rVcCW3kogJnLAnwIyf3FnzbX4wSjvUa88sAQ==", + "dependencies": { + "@react-aria/focus": "^3.17.1", + "@react-aria/overlays": "^3.22.1", + "@react-aria/utils": "^3.24.1", + "@react-types/dialog": "^3.5.10", + "@react-types/shared": "^3.23.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@react-aria/dnd": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/@react-aria/dnd/-/dnd-3.6.1.tgz", + "integrity": "sha512-6WnujUTD+cIYZVF/B+uXdHyJ+WSpbYa8jH282epvY4FUAq1qLmen12/HHcoj/5dswKQe8X6EM3OhkQM89d9vFw==", + "dependencies": { + "@internationalized/string": "^3.2.3", + "@react-aria/i18n": "^3.11.1", + "@react-aria/interactions": "^3.21.3", + "@react-aria/live-announcer": "^3.3.4", + "@react-aria/overlays": "^3.22.1", + "@react-aria/utils": "^3.24.1", + "@react-stately/dnd": "^3.3.1", + "@react-types/button": "^3.9.4", + "@react-types/shared": "^3.23.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@react-aria/focus": { + "version": "3.17.1", + "resolved": "https://registry.npmjs.org/@react-aria/focus/-/focus-3.17.1.tgz", + "integrity": "sha512-FLTySoSNqX++u0nWZJPPN5etXY0WBxaIe/YuL/GTEeuqUIuC/2bJSaw5hlsM6T2yjy6Y/VAxBcKSdAFUlU6njQ==", + "dependencies": { + "@react-aria/interactions": "^3.21.3", + "@react-aria/utils": "^3.24.1", + "@react-types/shared": "^3.23.1", + "@swc/helpers": "^0.5.0", + "clsx": "^2.0.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@react-aria/form": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@react-aria/form/-/form-3.0.5.tgz", + "integrity": "sha512-n290jRwrrRXO3fS82MyWR+OKN7yznVesy5Q10IclSTVYHHI3VI53xtAPr/WzNjJR1um8aLhOcDNFKwnNIUUCsQ==", + "dependencies": { + "@react-aria/interactions": "^3.21.3", + "@react-aria/utils": "^3.24.1", + "@react-stately/form": "^3.0.3", + "@react-types/shared": "^3.23.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@react-aria/grid": { + "version": "3.9.1", + "resolved": "https://registry.npmjs.org/@react-aria/grid/-/grid-3.9.1.tgz", + "integrity": "sha512-fGEZqAEaS8mqzV/II3N4ndoNWegIcbh+L3PmKbXdpKKUP8VgMs/WY5rYl5WAF0f5RoFwXqx3ibDLeR9tKj/bOg==", + "dependencies": { + "@react-aria/focus": "^3.17.1", + "@react-aria/i18n": "^3.11.1", + "@react-aria/interactions": "^3.21.3", + "@react-aria/live-announcer": "^3.3.4", + "@react-aria/selection": "^3.18.1", + "@react-aria/utils": "^3.24.1", + "@react-stately/collections": "^3.10.7", + "@react-stately/grid": "^3.8.7", + "@react-stately/selection": "^3.15.1", + "@react-stately/virtualizer": "^3.7.1", + "@react-types/checkbox": "^3.8.1", + "@react-types/grid": "^3.2.6", + "@react-types/shared": "^3.23.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@react-aria/gridlist": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/@react-aria/gridlist/-/gridlist-3.8.1.tgz", + "integrity": "sha512-vVPkkA+Ct0NDcpnNm/tnYaBumg0fP9pXxsPLqL1rxvsTyj1PaIpFTZ4corabPTbTDExZwUSTS3LG1n+o1OvBtQ==", + "dependencies": { + "@react-aria/focus": "^3.17.1", + "@react-aria/grid": "^3.9.1", + "@react-aria/i18n": "^3.11.1", + "@react-aria/interactions": "^3.21.3", + "@react-aria/selection": "^3.18.1", + "@react-aria/utils": "^3.24.1", + "@react-stately/collections": "^3.10.7", + "@react-stately/list": "^3.10.5", + "@react-stately/tree": "^3.8.1", + "@react-types/shared": "^3.23.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@react-aria/i18n": { + "version": "3.11.1", + "resolved": "https://registry.npmjs.org/@react-aria/i18n/-/i18n-3.11.1.tgz", + "integrity": "sha512-vuiBHw1kZruNMYeKkTGGnmPyMnM5T+gT8bz97H1FqIq1hQ6OPzmtBZ6W6l6OIMjeHI5oJo4utTwfZl495GALFQ==", + "dependencies": { + "@internationalized/date": "^3.5.4", + "@internationalized/message": "^3.1.4", + "@internationalized/number": "^3.5.3", + "@internationalized/string": "^3.2.3", + "@react-aria/ssr": "^3.9.4", + "@react-aria/utils": "^3.24.1", + "@react-types/shared": "^3.23.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@react-aria/interactions": { + "version": "3.21.3", + "resolved": "https://registry.npmjs.org/@react-aria/interactions/-/interactions-3.21.3.tgz", + "integrity": "sha512-BWIuf4qCs5FreDJ9AguawLVS0lV9UU+sK4CCnbCNNmYqOWY+1+gRXCsnOM32K+oMESBxilAjdHW5n1hsMqYMpA==", + "dependencies": { + "@react-aria/ssr": "^3.9.4", + "@react-aria/utils": "^3.24.1", + "@react-types/shared": "^3.23.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@react-aria/label": { + "version": "3.7.8", + "resolved": "https://registry.npmjs.org/@react-aria/label/-/label-3.7.8.tgz", + "integrity": "sha512-MzgTm5+suPA3KX7Ug6ZBK2NX9cin/RFLsv1BdafJ6CZpmUSpWnGE/yQfYUB7csN7j31OsZrD3/P56eShYWAQfg==", + "dependencies": { + "@react-aria/utils": "^3.24.1", + "@react-types/shared": "^3.23.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@react-aria/landmark": { + "version": "3.0.0-beta.12", + "resolved": "https://registry.npmjs.org/@react-aria/landmark/-/landmark-3.0.0-beta.12.tgz", + "integrity": "sha512-xCAYw0cxn115ZaTXyGXALW2Jebm56s7oX/R0/REubiHwkuDSRxRnYXbaaHxGXNsJWex5LGIZbUYaumGjGrzOnw==", + "dependencies": { + "@react-aria/utils": "^3.24.1", + "@react-types/shared": "^3.23.1", + "@swc/helpers": "^0.5.0", + "use-sync-external-store": "^1.2.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@react-aria/link": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/@react-aria/link/-/link-3.7.1.tgz", + "integrity": "sha512-a4IaV50P3fXc7DQvEIPYkJJv26JknFbRzFT5MJOMgtzuhyJoQdILEUK6XHYjcSSNCA7uLgzpojArVk5Hz3lCpw==", + "dependencies": { + "@react-aria/focus": "^3.17.1", + "@react-aria/interactions": "^3.21.3", + "@react-aria/utils": "^3.24.1", + "@react-types/link": "^3.5.5", + "@react-types/shared": "^3.23.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@react-aria/listbox": { + "version": "3.12.1", + "resolved": "https://registry.npmjs.org/@react-aria/listbox/-/listbox-3.12.1.tgz", + "integrity": "sha512-7JiUp0NGykbv/HgSpmTY1wqhuf/RmjFxs1HZcNaTv8A+DlzgJYc7yQqFjP3ZA/z5RvJFuuIxggIYmgIFjaRYdA==", + "dependencies": { + "@react-aria/interactions": "^3.21.3", + "@react-aria/label": "^3.7.8", + "@react-aria/selection": "^3.18.1", + "@react-aria/utils": "^3.24.1", + "@react-stately/collections": "^3.10.7", + "@react-stately/list": "^3.10.5", + "@react-types/listbox": "^3.4.9", + "@react-types/shared": "^3.23.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@react-aria/live-announcer": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@react-aria/live-announcer/-/live-announcer-3.3.4.tgz", + "integrity": "sha512-w8lxs35QrRrn6pBNzVfyGOeqWdxeVKf9U6bXIVwhq7rrTqRULL8jqy8RJIMfIs1s8G5FpwWYjyBOjl2g5Cu1iA==", + "dependencies": { + "@swc/helpers": "^0.5.0" + } + }, + "node_modules/@react-aria/menu": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/@react-aria/menu/-/menu-3.14.1.tgz", + "integrity": "sha512-BYliRb38uAzq05UOFcD5XkjA5foQoXRbcH3ZufBsc4kvh79BcP1PMW6KsXKGJ7dC/PJWUwCui6QL1kUg8PqMHA==", + "dependencies": { + "@react-aria/focus": "^3.17.1", + "@react-aria/i18n": "^3.11.1", + "@react-aria/interactions": "^3.21.3", + "@react-aria/overlays": "^3.22.1", + "@react-aria/selection": "^3.18.1", + "@react-aria/utils": "^3.24.1", + "@react-stately/collections": "^3.10.7", + "@react-stately/menu": "^3.7.1", + "@react-stately/tree": "^3.8.1", + "@react-types/button": "^3.9.4", + "@react-types/menu": "^3.9.9", + "@react-types/shared": "^3.23.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@react-aria/meter": { + "version": "3.4.13", + "resolved": "https://registry.npmjs.org/@react-aria/meter/-/meter-3.4.13.tgz", + "integrity": "sha512-oG6KvHQM3ri93XkYQkgEaMKSMO9KNDVpcW1MUqFfqyUXHFBRZRrJB4BTXMZ4nyjheFVQjVboU51fRwoLjOzThg==", + "dependencies": { + "@react-aria/progress": "^3.4.13", + "@react-types/meter": "^3.4.1", + "@react-types/shared": "^3.23.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@react-aria/numberfield": { + "version": "3.11.3", + "resolved": "https://registry.npmjs.org/@react-aria/numberfield/-/numberfield-3.11.3.tgz", + "integrity": "sha512-QQ9ZTzBbRI8d9ksaBWm6YVXbgv+5zzUsdxVxwzJVXLznvivoORB8rpdFJzUEWVCo25lzoBxluCEPYtLOxP1B0w==", + "dependencies": { + "@react-aria/i18n": "^3.11.1", + "@react-aria/interactions": "^3.21.3", + "@react-aria/spinbutton": "^3.6.5", + "@react-aria/textfield": "^3.14.5", + "@react-aria/utils": "^3.24.1", + "@react-stately/form": "^3.0.3", + "@react-stately/numberfield": "^3.9.3", + "@react-types/button": "^3.9.4", + "@react-types/numberfield": "^3.8.3", + "@react-types/shared": "^3.23.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@react-aria/overlays": { + "version": "3.22.1", + "resolved": "https://registry.npmjs.org/@react-aria/overlays/-/overlays-3.22.1.tgz", + "integrity": "sha512-GHiFMWO4EQ6+j6b5QCnNoOYiyx1Gk8ZiwLzzglCI4q1NY5AG2EAmfU4Z1+Gtrf2S5Y0zHbumC7rs9GnPoGLUYg==", + "dependencies": { + "@react-aria/focus": "^3.17.1", + "@react-aria/i18n": "^3.11.1", + "@react-aria/interactions": "^3.21.3", + "@react-aria/ssr": "^3.9.4", + "@react-aria/utils": "^3.24.1", + "@react-aria/visually-hidden": "^3.8.12", + "@react-stately/overlays": "^3.6.7", + "@react-types/button": "^3.9.4", + "@react-types/overlays": "^3.8.7", + "@react-types/shared": "^3.23.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@react-aria/progress": { + "version": "3.4.13", + "resolved": "https://registry.npmjs.org/@react-aria/progress/-/progress-3.4.13.tgz", + "integrity": "sha512-YBV9bOO5JzKvG8QCI0IAA00o6FczMgIDiK8Q9p5gKorFMatFUdRayxlbIPoYHMi+PguLil0jHgC7eOyaUcrZ0g==", + "dependencies": { + "@react-aria/i18n": "^3.11.1", + "@react-aria/label": "^3.7.8", + "@react-aria/utils": "^3.24.1", + "@react-types/progress": "^3.5.4", + "@react-types/shared": "^3.23.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@react-aria/radio": { + "version": "3.10.4", + "resolved": "https://registry.npmjs.org/@react-aria/radio/-/radio-3.10.4.tgz", + "integrity": "sha512-3fmoMcQtCpgjTwJReFjnvIE/C7zOZeCeWUn4JKDqz9s1ILYsC3Rk5zZ4q66tFn6v+IQnecrKT52wH6+hlVLwTA==", + "dependencies": { + "@react-aria/focus": "^3.17.1", + "@react-aria/form": "^3.0.5", + "@react-aria/i18n": "^3.11.1", + "@react-aria/interactions": "^3.21.3", + "@react-aria/label": "^3.7.8", + "@react-aria/utils": "^3.24.1", + "@react-stately/radio": "^3.10.4", + "@react-types/radio": "^3.8.1", + "@react-types/shared": "^3.23.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@react-aria/searchfield": { + "version": "3.7.5", + "resolved": "https://registry.npmjs.org/@react-aria/searchfield/-/searchfield-3.7.5.tgz", + "integrity": "sha512-h1sMUOWjhevaKKUHab/luHbM6yiyeN57L4RxZU0IIc9Ww0h5Rp2GUuKZA3pcdPiExHje0aijcImL3wBHEbKAzw==", + "dependencies": { + "@react-aria/i18n": "^3.11.1", + "@react-aria/textfield": "^3.14.5", + "@react-aria/utils": "^3.24.1", + "@react-stately/searchfield": "^3.5.3", + "@react-types/button": "^3.9.4", + "@react-types/searchfield": "^3.5.5", + "@react-types/shared": "^3.23.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@react-aria/select": { + "version": "3.14.5", + "resolved": "https://registry.npmjs.org/@react-aria/select/-/select-3.14.5.tgz", + "integrity": "sha512-s8jixBuTUNdKWRHe2tIJqp55ORHeUObGMw1s7PQRRVrrHPdNSYseAOI9B2W7qpl3hKhvjJg40UW+45mcb1WKbw==", + "dependencies": { + "@react-aria/form": "^3.0.5", + "@react-aria/i18n": "^3.11.1", + "@react-aria/interactions": "^3.21.3", + "@react-aria/label": "^3.7.8", + "@react-aria/listbox": "^3.12.1", + "@react-aria/menu": "^3.14.1", + "@react-aria/selection": "^3.18.1", + "@react-aria/utils": "^3.24.1", + "@react-aria/visually-hidden": "^3.8.12", + "@react-stately/select": "^3.6.4", + "@react-types/button": "^3.9.4", + "@react-types/select": "^3.9.4", + "@react-types/shared": "^3.23.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@react-aria/selection": { + "version": "3.18.1", + "resolved": "https://registry.npmjs.org/@react-aria/selection/-/selection-3.18.1.tgz", + "integrity": "sha512-GSqN2jX6lh7v+ldqhVjAXDcrWS3N4IsKXxO6L6Ygsye86Q9q9Mq9twWDWWu5IjHD6LoVZLUBCMO+ENGbOkyqeQ==", + "dependencies": { + "@react-aria/focus": "^3.17.1", + "@react-aria/i18n": "^3.11.1", + "@react-aria/interactions": "^3.21.3", + "@react-aria/utils": "^3.24.1", + "@react-stately/selection": "^3.15.1", + "@react-types/shared": "^3.23.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@react-aria/separator": { + "version": "3.3.13", + "resolved": "https://registry.npmjs.org/@react-aria/separator/-/separator-3.3.13.tgz", + "integrity": "sha512-hofA6JCPnAOqSE9vxnq7Dkazr7Kb2A0I5sR16fOG7ddjYRc/YEY5Nv7MWfKUGU0kNFHkgNjsDAILERtLechzeA==", + "dependencies": { + "@react-aria/utils": "^3.24.1", + "@react-types/shared": "^3.23.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@react-aria/slider": { + "version": "3.7.8", + "resolved": "https://registry.npmjs.org/@react-aria/slider/-/slider-3.7.8.tgz", + "integrity": "sha512-MYvPcM0K8jxEJJicUK2+WxUkBIM/mquBxOTOSSIL3CszA80nXIGVnLlCUnQV3LOUzpWtabbWaZokSPtGgOgQOw==", + "dependencies": { + "@react-aria/focus": "^3.17.1", + "@react-aria/i18n": "^3.11.1", + "@react-aria/interactions": "^3.21.3", + "@react-aria/label": "^3.7.8", + "@react-aria/utils": "^3.24.1", + "@react-stately/slider": "^3.5.4", + "@react-types/shared": "^3.23.1", + "@react-types/slider": "^3.7.3", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@react-aria/spinbutton": { + "version": "3.6.5", + "resolved": "https://registry.npmjs.org/@react-aria/spinbutton/-/spinbutton-3.6.5.tgz", + "integrity": "sha512-0aACBarF/Xr/7ixzjVBTQ0NBwwwsoGkf5v6AVFVMTC0uYMXHTALvRs+ULHjHMa5e/cX/aPlEvaVT7jfSs+Xy9Q==", + "dependencies": { + "@react-aria/i18n": "^3.11.1", + "@react-aria/live-announcer": "^3.3.4", + "@react-aria/utils": "^3.24.1", + "@react-types/button": "^3.9.4", + "@react-types/shared": "^3.23.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@react-aria/ssr": { + "version": "3.9.4", + "resolved": "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.9.4.tgz", + "integrity": "sha512-4jmAigVq409qcJvQyuorsmBR4+9r3+JEC60wC+Y0MZV0HCtTmm8D9guYXlJMdx0SSkgj0hHAyFm/HvPNFofCoQ==", + "dependencies": { + "@swc/helpers": "^0.5.0" + }, + "engines": { + "node": ">= 12" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@react-aria/switch": { + "version": "3.6.4", + "resolved": "https://registry.npmjs.org/@react-aria/switch/-/switch-3.6.4.tgz", + "integrity": "sha512-2nVqz4ZuJyof47IpGSt3oZRmp+EdS8wzeDYgf42WHQXrx4uEOk1mdLJ20+NnsYhj/2NHZsvXVrjBeKMjlMs+0w==", + "dependencies": { + "@react-aria/toggle": "^3.10.4", + "@react-stately/toggle": "^3.7.4", + "@react-types/switch": "^3.5.3", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@react-aria/table": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/@react-aria/table/-/table-3.14.1.tgz", + "integrity": "sha512-WaPgQe4zQF5OaluO5rm+Y2nEoFR63vsLd4BT4yjK1uaFhKhDY2Zk+1SCVQvBLLKS4WK9dhP05nrNzT0vp/ZPOw==", + "dependencies": { + "@react-aria/focus": "^3.17.1", + "@react-aria/grid": "^3.9.1", + "@react-aria/i18n": "^3.11.1", + "@react-aria/interactions": "^3.21.3", + "@react-aria/live-announcer": "^3.3.4", + "@react-aria/utils": "^3.24.1", + "@react-aria/visually-hidden": "^3.8.12", + "@react-stately/collections": "^3.10.7", + "@react-stately/flags": "^3.0.3", + "@react-stately/table": "^3.11.8", + "@react-stately/virtualizer": "^3.7.1", + "@react-types/checkbox": "^3.8.1", + "@react-types/grid": "^3.2.6", + "@react-types/shared": "^3.23.1", + "@react-types/table": "^3.9.5", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@react-aria/tabs": { + "version": "3.9.1", + "resolved": "https://registry.npmjs.org/@react-aria/tabs/-/tabs-3.9.1.tgz", + "integrity": "sha512-S5v/0sRcOaSXaJYZuuy1ZVzYc7JD4sDyseG1133GjyuNjJOFHgoWMb+b4uxNIJbZxnLgynn/ZDBZSO+qU+fIxw==", + "dependencies": { + "@react-aria/focus": "^3.17.1", + "@react-aria/i18n": "^3.11.1", + "@react-aria/selection": "^3.18.1", + "@react-aria/utils": "^3.24.1", + "@react-stately/tabs": "^3.6.6", + "@react-types/shared": "^3.23.1", + "@react-types/tabs": "^3.3.7", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@react-aria/tag": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/@react-aria/tag/-/tag-3.4.1.tgz", + "integrity": "sha512-gcIGPYZ2OBwMT4IHnlczEezKlxr0KRPL/mSfm2Q91GE027ZGOJnqusH9az6DX1qxrQx8x3vRdqYT2KmuefkrBQ==", + "dependencies": { + "@react-aria/gridlist": "^3.8.1", + "@react-aria/i18n": "^3.11.1", + "@react-aria/interactions": "^3.21.3", + "@react-aria/label": "^3.7.8", + "@react-aria/selection": "^3.18.1", + "@react-aria/utils": "^3.24.1", + "@react-stately/list": "^3.10.5", + "@react-types/button": "^3.9.4", + "@react-types/shared": "^3.23.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@react-aria/textfield": { + "version": "3.14.5", + "resolved": "https://registry.npmjs.org/@react-aria/textfield/-/textfield-3.14.5.tgz", + "integrity": "sha512-hj7H+66BjB1iTKKaFXwSZBZg88YT+wZboEXZ0DNdQB2ytzoz/g045wBItUuNi4ZjXI3P+0AOZznVMYadWBAmiA==", + "dependencies": { + "@react-aria/focus": "^3.17.1", + "@react-aria/form": "^3.0.5", + "@react-aria/label": "^3.7.8", + "@react-aria/utils": "^3.24.1", + "@react-stately/form": "^3.0.3", + "@react-stately/utils": "^3.10.1", + "@react-types/shared": "^3.23.1", + "@react-types/textfield": "^3.9.3", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@react-aria/toast": { + "version": "3.0.0-beta.12", + "resolved": "https://registry.npmjs.org/@react-aria/toast/-/toast-3.0.0-beta.12.tgz", + "integrity": "sha512-cNchqcvK+WJ+CTMzXBk8GzlsqEUUtbyLxz4BUgKHzysac50uhpbjdYbb+5cKetEaU6/wJ75VECk35wiLYkc16w==", + "dependencies": { + "@react-aria/i18n": "^3.11.1", + "@react-aria/interactions": "^3.21.3", + "@react-aria/landmark": "3.0.0-beta.12", + "@react-aria/utils": "^3.24.1", + "@react-stately/toast": "3.0.0-beta.4", + "@react-types/button": "^3.9.4", + "@react-types/shared": "^3.23.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@react-aria/toggle": { + "version": "3.10.4", + "resolved": "https://registry.npmjs.org/@react-aria/toggle/-/toggle-3.10.4.tgz", + "integrity": "sha512-bRk+CdB8QzrSyGNjENXiTWxfzYKRw753iwQXsEAU7agPCUdB8cZJyrhbaUoD0rwczzTp2zDbZ9rRbUPdsBE2YQ==", + "dependencies": { + "@react-aria/focus": "^3.17.1", + "@react-aria/interactions": "^3.21.3", + "@react-aria/utils": "^3.24.1", + "@react-stately/toggle": "^3.7.4", + "@react-types/checkbox": "^3.8.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@react-aria/toolbar": { + "version": "3.0.0-beta.5", + "resolved": "https://registry.npmjs.org/@react-aria/toolbar/-/toolbar-3.0.0-beta.5.tgz", + "integrity": "sha512-c8spY7aeLI6L+ygdXvEbAzaT41vExsxZ1Ld0t7BB+6iEF3nyBNJHshjkgdR7nv8FLgNk0no4tj0GTq4Jj4UqHQ==", + "dependencies": { + "@react-aria/focus": "^3.17.1", + "@react-aria/i18n": "^3.11.1", + "@react-aria/utils": "^3.24.1", + "@react-types/shared": "^3.23.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@react-aria/tooltip": { + "version": "3.7.4", + "resolved": "https://registry.npmjs.org/@react-aria/tooltip/-/tooltip-3.7.4.tgz", + "integrity": "sha512-+XRx4HlLYqWY3fB8Z60bQi/rbWDIGlFUtXYbtoa1J+EyRWfhpvsYImP8qeeNO/vgjUtDy1j9oKa8p6App9mBMQ==", + "dependencies": { + "@react-aria/focus": "^3.17.1", + "@react-aria/interactions": "^3.21.3", + "@react-aria/utils": "^3.24.1", + "@react-stately/tooltip": "^3.4.9", + "@react-types/shared": "^3.23.1", + "@react-types/tooltip": "^3.4.9", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@react-aria/tree": { + "version": "3.0.0-alpha.1", + "resolved": "https://registry.npmjs.org/@react-aria/tree/-/tree-3.0.0-alpha.1.tgz", + "integrity": "sha512-CucyeJ4VeAvWO5UJHt/l9JO65CVtsOVUctMOVNCQS77Isqp3olX9pvfD3LXt8fD5Ph2g0Q/b7siVpX5ieVB32g==", + "dependencies": { + "@react-aria/gridlist": "^3.8.1", + "@react-aria/i18n": "^3.11.1", + "@react-aria/selection": "^3.18.1", + "@react-aria/utils": "^3.24.1", + "@react-stately/tree": "^3.8.1", + "@react-types/button": "^3.9.4", + "@react-types/shared": "^3.23.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@react-aria/utils": { + "version": "3.24.1", + "resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.24.1.tgz", + "integrity": "sha512-O3s9qhPMd6n42x9sKeJ3lhu5V1Tlnzhu6Yk8QOvDuXf7UGuUjXf9mzfHJt1dYzID4l9Fwm8toczBzPM9t0jc8Q==", + "dependencies": { + "@react-aria/ssr": "^3.9.4", + "@react-stately/utils": "^3.10.1", + "@react-types/shared": "^3.23.1", + "@swc/helpers": "^0.5.0", + "clsx": "^2.0.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@react-aria/visually-hidden": { + "version": "3.8.12", + "resolved": "https://registry.npmjs.org/@react-aria/visually-hidden/-/visually-hidden-3.8.12.tgz", + "integrity": "sha512-Bawm+2Cmw3Xrlr7ARzl2RLtKh0lNUdJ0eNqzWcyx4c0VHUAWtThmH5l+HRqFUGzzutFZVo89SAy40BAbd0gjVw==", + "dependencies": { + "@react-aria/interactions": "^3.21.3", + "@react-aria/utils": "^3.24.1", + "@react-types/shared": "^3.23.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@react-stately/calendar": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/@react-stately/calendar/-/calendar-3.5.1.tgz", + "integrity": "sha512-7l7QhqGUJ5AzWHfvZzbTe3J4t72Ht5BmhW4hlVI7flQXtfrmYkVtl3ZdytEZkkHmWGYZRW9b4IQTQGZxhtlElA==", + "dependencies": { + "@internationalized/date": "^3.5.4", + "@react-stately/utils": "^3.10.1", + "@react-types/calendar": "^3.4.6", + "@react-types/shared": "^3.23.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@react-stately/checkbox": { + "version": "3.6.5", + "resolved": "https://registry.npmjs.org/@react-stately/checkbox/-/checkbox-3.6.5.tgz", + "integrity": "sha512-IXV3f9k+LtmfQLE+DKIN41Q5QB/YBLDCB1YVx5PEdRp52S9+EACD5683rjVm8NVRDwjMi2SP6RnFRk7fVb5Azg==", + "dependencies": { + "@react-stately/form": "^3.0.3", + "@react-stately/utils": "^3.10.1", + "@react-types/checkbox": "^3.8.1", + "@react-types/shared": "^3.23.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@react-stately/collections": { + "version": "3.10.7", + "resolved": "https://registry.npmjs.org/@react-stately/collections/-/collections-3.10.7.tgz", + "integrity": "sha512-KRo5O2MWVL8n3aiqb+XR3vP6akmHLhLWYZEmPKjIv0ghQaEebBTrN3wiEjtd6dzllv0QqcWvDLM1LntNfJ2TsA==", + "dependencies": { + "@react-types/shared": "^3.23.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@react-stately/color": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/@react-stately/color/-/color-3.6.1.tgz", + "integrity": "sha512-iW0nAhl3+fUBegHMw5EcAbFVDpgwHBrivfC85pVoTM3pyzp66hqNN6R6xWxW6ETyljS8UOer59+/w4GDVGdPig==", + "dependencies": { + "@internationalized/number": "^3.5.3", + "@internationalized/string": "^3.2.3", + "@react-aria/i18n": "^3.11.1", + "@react-stately/form": "^3.0.3", + "@react-stately/numberfield": "^3.9.3", + "@react-stately/slider": "^3.5.4", + "@react-stately/utils": "^3.10.1", + "@react-types/color": "3.0.0-beta.25", + "@react-types/shared": "^3.23.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@react-stately/combobox": { + "version": "3.8.4", + "resolved": "https://registry.npmjs.org/@react-stately/combobox/-/combobox-3.8.4.tgz", + "integrity": "sha512-iLVGvKRRz0TeJXZhZyK783hveHpYA6xovOSdzSD+WGYpiPXo1QrcrNoH3AE0Z2sHtorU+8nc0j58vh5PB+m2AA==", + "dependencies": { + "@react-stately/collections": "^3.10.7", + "@react-stately/form": "^3.0.3", + "@react-stately/list": "^3.10.5", + "@react-stately/overlays": "^3.6.7", + "@react-stately/select": "^3.6.4", + "@react-stately/utils": "^3.10.1", + "@react-types/combobox": "^3.11.1", + "@react-types/shared": "^3.23.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@react-stately/data": { + "version": "3.11.4", + "resolved": "https://registry.npmjs.org/@react-stately/data/-/data-3.11.4.tgz", + "integrity": "sha512-PbnUQxeE6AznSuEWYnRmrYQ9t5z1Asx98Jtrl96EeA6Iapt9kOjTN9ySqCxtPxMKleb1NIqG3+uHU3veIqmLsg==", + "dependencies": { + "@react-types/shared": "^3.23.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@react-stately/datepicker": { + "version": "3.9.4", + "resolved": "https://registry.npmjs.org/@react-stately/datepicker/-/datepicker-3.9.4.tgz", + "integrity": "sha512-yBdX01jn6gq4NIVvHIqdjBUPo+WN8Bujc4OnPw+ZnfA4jI0eIgq04pfZ84cp1LVXW0IB0VaCu1AlQ/kvtZjfGA==", + "dependencies": { + "@internationalized/date": "^3.5.4", + "@internationalized/string": "^3.2.3", + "@react-stately/form": "^3.0.3", + "@react-stately/overlays": "^3.6.7", + "@react-stately/utils": "^3.10.1", + "@react-types/datepicker": "^3.7.4", + "@react-types/shared": "^3.23.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@react-stately/dnd": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@react-stately/dnd/-/dnd-3.3.1.tgz", + "integrity": "sha512-I/Ci5xB8hSgAXzoWYWScfMM9UK1MX/eTlARBhiSlfudewweOtNJAI+cXJgU7uiUnGjh4B4v3qDBtlAH1dWDCsw==", + "dependencies": { + "@react-stately/selection": "^3.15.1", + "@react-types/shared": "^3.23.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@react-stately/flags": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@react-stately/flags/-/flags-3.0.3.tgz", + "integrity": "sha512-/ha7XFA0RZTQsbzSPwu3KkbNMgbvuM0GuMTYLTBWpgBrovBNTM+QqI/PfZTdHg8PwCYF4H5Y8gjdSpdulCvJFw==", + "dependencies": { + "@swc/helpers": "^0.5.0" + } + }, + "node_modules/@react-stately/form": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@react-stately/form/-/form-3.0.3.tgz", + "integrity": "sha512-92YYBvlHEWUGUpXgIaQ48J50jU9XrxfjYIN8BTvvhBHdD63oWgm8DzQnyT/NIAMzdLnhkg7vP+fjG8LjHeyIAg==", + "dependencies": { + "@react-types/shared": "^3.23.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@react-stately/grid": { + "version": "3.8.7", + "resolved": "https://registry.npmjs.org/@react-stately/grid/-/grid-3.8.7.tgz", + "integrity": "sha512-he3TXCLAhF5C5z1/G4ySzcwyt7PEiWcVIupxebJQqRyFrNWemSuv+7tolnStmG8maMVIyV3P/3j4eRBbdSlOIg==", + "dependencies": { + "@react-stately/collections": "^3.10.7", + "@react-stately/selection": "^3.15.1", + "@react-types/grid": "^3.2.6", + "@react-types/shared": "^3.23.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@react-stately/list": { + "version": "3.10.5", + "resolved": "https://registry.npmjs.org/@react-stately/list/-/list-3.10.5.tgz", + "integrity": "sha512-fV9plO+6QDHiewsYIhboxcDhF17GO95xepC5ki0bKXo44gr14g/LSo/BMmsaMnV+1BuGdBunB05bO4QOIaigXA==", + "dependencies": { + "@react-stately/collections": "^3.10.7", + "@react-stately/selection": "^3.15.1", + "@react-stately/utils": "^3.10.1", + "@react-types/shared": "^3.23.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@react-stately/menu": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/@react-stately/menu/-/menu-3.7.1.tgz", + "integrity": "sha512-mX1w9HHzt+xal1WIT2xGrTQsoLvDwuB2R1Er1MBABs//MsJzccycatcgV/J/28m6tO5M9iuFQQvLV+i1dCtodg==", + "dependencies": { + "@react-stately/overlays": "^3.6.7", + "@react-types/menu": "^3.9.9", + "@react-types/shared": "^3.23.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@react-stately/numberfield": { + "version": "3.9.3", + "resolved": "https://registry.npmjs.org/@react-stately/numberfield/-/numberfield-3.9.3.tgz", + "integrity": "sha512-UlPTLSabhLEuHtgzM0PgfhtEaHy3yttbzcRb8yHNvGo4KbCHeHpTHd3QghKfTFm024Mug7+mVlWCmMtW0f5ttg==", + "dependencies": { + "@internationalized/number": "^3.5.3", + "@react-stately/form": "^3.0.3", + "@react-stately/utils": "^3.10.1", + "@react-types/numberfield": "^3.8.3", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@react-stately/overlays": { + "version": "3.6.7", + "resolved": "https://registry.npmjs.org/@react-stately/overlays/-/overlays-3.6.7.tgz", + "integrity": "sha512-6zp8v/iNUm6YQap0loaFx6PlvN8C0DgWHNlrlzMtMmNuvjhjR0wYXVaTfNoUZBWj25tlDM81ukXOjpRXg9rLrw==", + "dependencies": { + "@react-stately/utils": "^3.10.1", + "@react-types/overlays": "^3.8.7", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@react-stately/radio": { + "version": "3.10.4", + "resolved": "https://registry.npmjs.org/@react-stately/radio/-/radio-3.10.4.tgz", + "integrity": "sha512-kCIc7tAl4L7Hu4Wt9l2jaa+MzYmAJm0qmC8G8yPMbExpWbLRu6J8Un80GZu+JxvzgDlqDyrVvyv9zFifwH/NkQ==", + "dependencies": { + "@react-stately/form": "^3.0.3", + "@react-stately/utils": "^3.10.1", + "@react-types/radio": "^3.8.1", + "@react-types/shared": "^3.23.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@react-stately/searchfield": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/@react-stately/searchfield/-/searchfield-3.5.3.tgz", + "integrity": "sha512-H0OvlgwPIFdc471ypw79MDjz3WXaVq9+THaY6JM4DIohEJNN5Dwei7O9g6r6m/GqPXJIn5TT3b74kJ2Osc00YQ==", + "dependencies": { + "@react-stately/utils": "^3.10.1", + "@react-types/searchfield": "^3.5.5", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@react-stately/select": { + "version": "3.6.4", + "resolved": "https://registry.npmjs.org/@react-stately/select/-/select-3.6.4.tgz", + "integrity": "sha512-whZgF1N53D0/dS8tOFdrswB0alsk5Q5620HC3z+5f2Hpi8gwgAZ8TYa+2IcmMYRiT+bxVuvEc/NirU9yPmqGbA==", + "dependencies": { + "@react-stately/form": "^3.0.3", + "@react-stately/list": "^3.10.5", + "@react-stately/overlays": "^3.6.7", + "@react-types/select": "^3.9.4", + "@react-types/shared": "^3.23.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@react-stately/selection": { + "version": "3.15.1", + "resolved": "https://registry.npmjs.org/@react-stately/selection/-/selection-3.15.1.tgz", + "integrity": "sha512-6TQnN9L0UY9w19B7xzb1P6mbUVBtW840Cw1SjgNXCB3NPaCf59SwqClYzoj8O2ZFzMe8F/nUJtfU1NS65/OLlw==", + "dependencies": { + "@react-stately/collections": "^3.10.7", + "@react-stately/utils": "^3.10.1", + "@react-types/shared": "^3.23.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@react-stately/slider": { + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/@react-stately/slider/-/slider-3.5.4.tgz", + "integrity": "sha512-Jsf7K17dr93lkNKL9ij8HUcoM1sPbq8TvmibD6DhrK9If2lje+OOL8y4n4qreUnfMT56HCAeS9wCO3fg3eMyrw==", + "dependencies": { + "@react-stately/utils": "^3.10.1", + "@react-types/shared": "^3.23.1", + "@react-types/slider": "^3.7.3", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@react-stately/table": { + "version": "3.11.8", + "resolved": "https://registry.npmjs.org/@react-stately/table/-/table-3.11.8.tgz", + "integrity": "sha512-EdyRW3lT1/kAVDp5FkEIi1BQ7tvmD2YgniGdLuW/l9LADo0T+oxZqruv60qpUS6sQap+59Riaxl91ClDxrJnpg==", + "dependencies": { + "@react-stately/collections": "^3.10.7", + "@react-stately/flags": "^3.0.3", + "@react-stately/grid": "^3.8.7", + "@react-stately/selection": "^3.15.1", + "@react-stately/utils": "^3.10.1", + "@react-types/grid": "^3.2.6", + "@react-types/shared": "^3.23.1", + "@react-types/table": "^3.9.5", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@react-stately/tabs": { + "version": "3.6.6", + "resolved": "https://registry.npmjs.org/@react-stately/tabs/-/tabs-3.6.6.tgz", + "integrity": "sha512-sOLxorH2uqjAA+v1ppkMCc2YyjgqvSGeBDgtR/lyPSDd4CVMoTExszROX2dqG0c8il9RQvzFuufUtQWMY6PgSA==", + "dependencies": { + "@react-stately/list": "^3.10.5", + "@react-types/shared": "^3.23.1", + "@react-types/tabs": "^3.3.7", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@react-stately/toast": { + "version": "3.0.0-beta.4", + "resolved": "https://registry.npmjs.org/@react-stately/toast/-/toast-3.0.0-beta.4.tgz", + "integrity": "sha512-YzEyFNAAQqZ+7pZpRiejotxDzp5CHagN21btVGGwU2jHu/oQRR+todtB3wADAp7EF5lfAlAsLABiD9ZNWQeDTw==", + "dependencies": { + "@swc/helpers": "^0.5.0", + "use-sync-external-store": "^1.2.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@react-stately/toggle": { + "version": "3.7.4", + "resolved": "https://registry.npmjs.org/@react-stately/toggle/-/toggle-3.7.4.tgz", + "integrity": "sha512-CoYFe9WrhLkDP4HGDpJYQKwfiYCRBAeoBQHv+JWl5eyK61S8xSwoHsveYuEZ3bowx71zyCnNAqWRrmNOxJ4CKA==", + "dependencies": { + "@react-stately/utils": "^3.10.1", + "@react-types/checkbox": "^3.8.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@react-stately/tooltip": { + "version": "3.4.9", + "resolved": "https://registry.npmjs.org/@react-stately/tooltip/-/tooltip-3.4.9.tgz", + "integrity": "sha512-P7CDJsdoKarz32qFwf3VNS01lyC+63gXpDZG31pUu+EO5BeQd4WKN/AH1Beuswpr4GWzxzFc1aXQgERFGVzraA==", + "dependencies": { + "@react-stately/overlays": "^3.6.7", + "@react-types/tooltip": "^3.4.9", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@react-stately/tree": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/@react-stately/tree/-/tree-3.8.1.tgz", + "integrity": "sha512-LOdkkruJWch3W89h4B/bXhfr0t0t1aRfEp+IMrrwdRAl23NaPqwl5ILHs4Xu5XDHqqhg8co73pHrJwUyiTWEjw==", + "dependencies": { + "@react-stately/collections": "^3.10.7", + "@react-stately/selection": "^3.15.1", + "@react-stately/utils": "^3.10.1", + "@react-types/shared": "^3.23.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@react-stately/utils": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/@react-stately/utils/-/utils-3.10.1.tgz", + "integrity": "sha512-VS/EHRyicef25zDZcM/ClpzYMC5i2YGN6uegOeQawmgfGjb02yaCX0F0zR69Pod9m2Hr3wunTbtpgVXvYbZItg==", + "dependencies": { + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@react-stately/virtualizer": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/@react-stately/virtualizer/-/virtualizer-3.7.1.tgz", + "integrity": "sha512-voHgE6EQ+oZaLv6u2umKxakvIKNkCQuUihqKACTjdslp7SJh4Mvs3oLBI0hf0JOh+rCcFIKDvQtFwy1fXFRYBA==", + "dependencies": { + "@react-aria/utils": "^3.24.1", + "@react-types/shared": "^3.23.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@react-types/breadcrumbs": { + "version": "3.7.5", + "resolved": "https://registry.npmjs.org/@react-types/breadcrumbs/-/breadcrumbs-3.7.5.tgz", + "integrity": "sha512-lV9IDYsMiu2TgdMIjEmsOE0YWwjb3jhUNK1DCZZfq6uWuiHLgyx2EncazJBUWSjHJ4ta32j7xTuXch+8Ai6u/A==", + "dependencies": { + "@react-types/link": "^3.5.5", + "@react-types/shared": "^3.23.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@react-types/button": { + "version": "3.9.4", + "resolved": "https://registry.npmjs.org/@react-types/button/-/button-3.9.4.tgz", + "integrity": "sha512-raeQBJUxBp0axNF74TXB8/H50GY8Q3eV6cEKMbZFP1+Dzr09Ngv0tJBeW0ewAxAguNH5DRoMUAUGIXtSXskVdA==", + "dependencies": { + "@react-types/shared": "^3.23.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@react-types/calendar": { + "version": "3.4.6", + "resolved": "https://registry.npmjs.org/@react-types/calendar/-/calendar-3.4.6.tgz", + "integrity": "sha512-WSntZPwtvsIYWvBQRAPvuCn55UTJBZroTvX0vQvWykJRQnPAI20G1hMQ3dNsnAL+gLZUYxBXn66vphmjUuSYew==", + "dependencies": { + "@internationalized/date": "^3.5.4", + "@react-types/shared": "^3.23.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@react-types/checkbox": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/@react-types/checkbox/-/checkbox-3.8.1.tgz", + "integrity": "sha512-5/oVByPw4MbR/8QSdHCaalmyWC71H/QGgd4aduTJSaNi825o+v/hsN2/CH7Fq9atkLKsC8fvKD00Bj2VGaKriQ==", + "dependencies": { + "@react-types/shared": "^3.23.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@react-types/color": { + "version": "3.0.0-beta.25", + "resolved": "https://registry.npmjs.org/@react-types/color/-/color-3.0.0-beta.25.tgz", + "integrity": "sha512-D24ASvLeSWouBwOBi4ftUe4/BhrZj5AiHV7tXwrVeMGOy9Z9jyeK65Xysq+R3ecaSONLXsgai5CQMvj13cOacA==", + "dependencies": { + "@react-types/shared": "^3.23.1", + "@react-types/slider": "^3.7.3" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@react-types/combobox": { + "version": "3.11.1", + "resolved": "https://registry.npmjs.org/@react-types/combobox/-/combobox-3.11.1.tgz", + "integrity": "sha512-UNc3OHt5cUt5gCTHqhQIqhaWwKCpaNciD8R7eQazmHiA9fq8ROlV+7l3gdNgdhJbTf5Bu/V5ISnN7Y1xwL3zqQ==", + "dependencies": { + "@react-types/shared": "^3.23.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@react-types/datepicker": { + "version": "3.7.4", + "resolved": "https://registry.npmjs.org/@react-types/datepicker/-/datepicker-3.7.4.tgz", + "integrity": "sha512-ZfvgscvNzBJpYyVWg3nstJtA/VlWLwErwSkd1ivZYam859N30w8yH+4qoYLa6FzWLCFlrsRHyvtxlEM7lUAt5A==", + "dependencies": { + "@internationalized/date": "^3.5.4", + "@react-types/calendar": "^3.4.6", + "@react-types/overlays": "^3.8.7", + "@react-types/shared": "^3.23.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@react-types/dialog": { + "version": "3.5.10", + "resolved": "https://registry.npmjs.org/@react-types/dialog/-/dialog-3.5.10.tgz", + "integrity": "sha512-S9ga+edOLNLZw7/zVOnZdT5T40etpzUYBXEKdFPbxyPYnERvRxJAsC1/ASuBU9fQAXMRgLZzADWV+wJoGS/X9g==", + "dependencies": { + "@react-types/overlays": "^3.8.7", + "@react-types/shared": "^3.23.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@react-types/form": { + "version": "3.7.4", + "resolved": "https://registry.npmjs.org/@react-types/form/-/form-3.7.4.tgz", + "integrity": "sha512-HZojAWrb6feYnhDEOy3vBamDVAHDl0l2JQZ7aIDLHmeTAGQC3JNZcm2fLTxqLye46zz8w8l8OHgI+NdD4PHdOw==", + "dependencies": { + "@react-types/shared": "^3.23.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@react-types/grid": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@react-types/grid/-/grid-3.2.6.tgz", + "integrity": "sha512-XfHenL2jEBUYrhKiPdeM24mbLRXUn79wVzzMhrNYh24nBwhsPPpxF+gjFddT3Cy8dt6tRInfT6pMEu9nsXwaHw==", + "dependencies": { + "@react-types/shared": "^3.23.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@react-types/link": { + "version": "3.5.5", + "resolved": "https://registry.npmjs.org/@react-types/link/-/link-3.5.5.tgz", + "integrity": "sha512-G6P5WagHDR87npN7sEuC5IIgL1GsoY4WFWKO4734i2CXRYx24G9P0Su3AX4GA3qpspz8sK1AWkaCzBMmvnunfw==", + "dependencies": { + "@react-types/shared": "^3.23.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@react-types/listbox": { + "version": "3.4.9", + "resolved": "https://registry.npmjs.org/@react-types/listbox/-/listbox-3.4.9.tgz", + "integrity": "sha512-S5G+WmNKUIOPZxZ4svWwWQupP3C6LmVfnf8QQmPDvwYXGzVc0WovkqUWyhhjJirFDswTXRCO9p0yaTHHIlkdwQ==", + "dependencies": { + "@react-types/shared": "^3.23.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@react-types/menu": { + "version": "3.9.9", + "resolved": "https://registry.npmjs.org/@react-types/menu/-/menu-3.9.9.tgz", + "integrity": "sha512-FamUaPVs1Fxr4KOMI0YcR2rYZHoN7ypGtgiEiJ11v/tEPjPPGgeKDxii0McCrdOkjheatLN1yd2jmMwYj6hTDg==", + "dependencies": { + "@react-types/overlays": "^3.8.7", + "@react-types/shared": "^3.23.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@react-types/meter": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/@react-types/meter/-/meter-3.4.1.tgz", + "integrity": "sha512-AIJV4NDFAqKH94s02c5Da4TH2qgJjfrw978zuFM0KUBFD85WRPKh7MvgWpomvUgmzqE6lMCzIdi1KPKqrRabdw==", + "dependencies": { + "@react-types/progress": "^3.5.4" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@react-types/numberfield": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/@react-types/numberfield/-/numberfield-3.8.3.tgz", + "integrity": "sha512-z5fGfVj3oh5bmkw9zDvClA1nDBSFL9affOuyk2qZ/M2SRUmykDAPCksbfcMndft0XULWKbF4s2CYbVI+E/yrUA==", + "dependencies": { + "@react-types/shared": "^3.23.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@react-types/overlays": { + "version": "3.8.7", + "resolved": "https://registry.npmjs.org/@react-types/overlays/-/overlays-3.8.7.tgz", + "integrity": "sha512-zCOYvI4at2DkhVpviIClJ7bRrLXYhSg3Z3v9xymuPH3mkiuuP/dm8mUCtkyY4UhVeUTHmrQh1bzaOP00A+SSQA==", + "dependencies": { + "@react-types/shared": "^3.23.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@react-types/progress": { + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/@react-types/progress/-/progress-3.5.4.tgz", + "integrity": "sha512-JNc246sTjasPyx5Dp7/s0rp3Bz4qlu4LrZTulZlxWyb53WgBNL7axc26CCi+I20rWL9+c7JjhrRxnLl/1cLN5g==", + "dependencies": { + "@react-types/shared": "^3.23.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@react-types/radio": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/@react-types/radio/-/radio-3.8.1.tgz", + "integrity": "sha512-bK0gio/qj1+0Ldu/3k/s9BaOZvnnRgvFtL3u5ky479+aLG5qf1CmYed3SKz8ErZ70JkpuCSrSwSCFf0t1IHovw==", + "dependencies": { + "@react-types/shared": "^3.23.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@react-types/searchfield": { + "version": "3.5.5", + "resolved": "https://registry.npmjs.org/@react-types/searchfield/-/searchfield-3.5.5.tgz", + "integrity": "sha512-T/NHg12+w23TxlXMdetogLDUldk1z5dDavzbnjKrLkajLb221bp8brlR/+O6C1CtFpuJGALqYHgTasU1qkQFSA==", + "dependencies": { + "@react-types/shared": "^3.23.1", + "@react-types/textfield": "^3.9.3" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@react-types/select": { + "version": "3.9.4", + "resolved": "https://registry.npmjs.org/@react-types/select/-/select-3.9.4.tgz", + "integrity": "sha512-xI7dnOW2st91fPPcv6hdtrTdcfetYiqZuuVPZ5TRobY7Q10/Zqqe/KqtOw1zFKUj9xqNJe4Ov3xP5GSdcO60Eg==", + "dependencies": { + "@react-types/shared": "^3.23.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@react-types/shared": { + "version": "3.23.1", + "resolved": "https://registry.npmjs.org/@react-types/shared/-/shared-3.23.1.tgz", + "integrity": "sha512-5d+3HbFDxGZjhbMBeFHRQhexMFt4pUce3okyRtUVKbbedQFUrtXSBg9VszgF2RTeQDKDkMCIQDtz5ccP/Lk1gw==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@react-types/slider": { + "version": "3.7.3", + "resolved": "https://registry.npmjs.org/@react-types/slider/-/slider-3.7.3.tgz", + "integrity": "sha512-F8qFQaD2mqug2D0XeWMmjGBikiwbdERFlhFzdvNGbypPLz3AZICBKp1ZLPWdl0DMuy03G/jy6Gl4mDobl7RT2g==", + "dependencies": { + "@react-types/shared": "^3.23.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@react-types/switch": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/@react-types/switch/-/switch-3.5.3.tgz", + "integrity": "sha512-Nb6+J5MrPaFa8ZNFKGMzAsen/NNzl5UG/BbC65SLGPy7O0VDa/sUpn7dcu8V2xRpRwwIN/Oso4v63bt2sgdkgA==", + "dependencies": { + "@react-types/shared": "^3.23.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@react-types/table": { + "version": "3.9.5", + "resolved": "https://registry.npmjs.org/@react-types/table/-/table-3.9.5.tgz", + "integrity": "sha512-fgM2j9F/UR4Anmd28CueghCgBwOZoCVyN8fjaIFPd2MN4gCwUUfANwxLav65gZk4BpwUXGoQdsW+X50L3555mg==", + "dependencies": { + "@react-types/grid": "^3.2.6", + "@react-types/shared": "^3.23.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@react-types/tabs": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/@react-types/tabs/-/tabs-3.3.7.tgz", + "integrity": "sha512-ZdLe5xOcFX6+/ni45Dl2jO0jFATpTnoSqj6kLIS/BYv8oh0n817OjJkLf+DS3CLfNjApJWrHqAk34xNh6nRnEg==", + "dependencies": { + "@react-types/shared": "^3.23.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@react-types/textfield": { + "version": "3.9.3", + "resolved": "https://registry.npmjs.org/@react-types/textfield/-/textfield-3.9.3.tgz", + "integrity": "sha512-DoAY6cYOL0pJhgNGI1Rosni7g72GAt4OVr2ltEx2S9ARmFZ0DBvdhA9lL2nywcnKMf27PEJcKMXzXc10qaHsJw==", + "dependencies": { + "@react-types/shared": "^3.23.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@react-types/tooltip": { + "version": "3.4.9", + "resolved": "https://registry.npmjs.org/@react-types/tooltip/-/tooltip-3.4.9.tgz", + "integrity": "sha512-wZ+uF1+Zc43qG+cOJzioBmLUNjRa7ApdcT0LI1VvaYvH5GdfjzUJOorLX9V/vAci0XMJ50UZ+qsh79aUlw2yqg==", + "dependencies": { + "@react-types/overlays": "^3.8.7", + "@react-types/shared": "^3.23.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@remix-run/router": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.9.0.tgz", + "integrity": "sha512-bV63itrKBC0zdT27qYm6SDZHlkXwFL1xMBuhkn+X7l0+IIhNaH5wuuvZKp6eKhCD4KFhujhfhCT1YxXW6esUIA==", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rollup/pluginutils": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.0.tgz", + "integrity": "sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==", + "dev": true, + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.18.0.tgz", + "integrity": "sha512-Tya6xypR10giZV1XzxmH5wr25VcZSncG0pZIjfePT0OVBvqNEurzValetGNarVrGiq66EBVAFn15iYX4w6FKgQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.18.0.tgz", + "integrity": "sha512-avCea0RAP03lTsDhEyfy+hpfr85KfyTctMADqHVhLAF3MlIkq83CP8UfAHUssgXTYd+6er6PaAhx/QGv4L1EiA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.18.0.tgz", + "integrity": "sha512-IWfdwU7KDSm07Ty0PuA/W2JYoZ4iTj3TUQjkVsO/6U+4I1jN5lcR71ZEvRh52sDOERdnNhhHU57UITXz5jC1/w==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.18.0.tgz", + "integrity": "sha512-n2LMsUz7Ynu7DoQrSQkBf8iNrjOGyPLrdSg802vk6XT3FtsgX6JbE8IHRvposskFm9SNxzkLYGSq9QdpLYpRNA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.18.0.tgz", + "integrity": "sha512-C/zbRYRXFjWvz9Z4haRxcTdnkPt1BtCkz+7RtBSuNmKzMzp3ZxdM28Mpccn6pt28/UWUCTXa+b0Mx1k3g6NOMA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.18.0.tgz", + "integrity": "sha512-l3m9ewPgjQSXrUMHg93vt0hYCGnrMOcUpTz6FLtbwljo2HluS4zTXFy2571YQbisTnfTKPZ01u/ukJdQTLGh9A==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.18.0.tgz", + "integrity": "sha512-rJ5D47d8WD7J+7STKdCUAgmQk49xuFrRi9pZkWoRD1UeSMakbcepWXPF8ycChBoAqs1pb2wzvbY6Q33WmN2ftw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.18.0.tgz", + "integrity": "sha512-be6Yx37b24ZwxQ+wOQXXLZqpq4jTckJhtGlWGZs68TgdKXJgw54lUUoFYrg6Zs/kjzAQwEwYbp8JxZVzZLRepQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.18.0.tgz", + "integrity": "sha512-hNVMQK+qrA9Todu9+wqrXOHxFiD5YmdEi3paj6vP02Kx1hjd2LLYR2eaN7DsEshg09+9uzWi2W18MJDlG0cxJA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.18.0.tgz", + "integrity": "sha512-ROCM7i+m1NfdrsmvwSzoxp9HFtmKGHEqu5NNDiZWQtXLA8S5HBCkVvKAxJ8U+CVctHwV2Gb5VUaK7UAkzhDjlg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.18.0.tgz", + "integrity": "sha512-0UyyRHyDN42QL+NbqevXIIUnKA47A+45WyasO+y2bGJ1mhQrfrtXUpTxCOrfxCR4esV3/RLYyucGVPiUsO8xjg==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.18.0.tgz", + "integrity": "sha512-xuglR2rBVHA5UsI8h8UbX4VJ470PtGCf5Vpswh7p2ukaqBGFTnsfzxUBetoWBWymHMxbIG0Cmx7Y9qDZzr648w==", + "cpu": [ + "x64" + ], "dev": true, "optional": true, "os": [ @@ -1165,36 +3687,257 @@ "arm64" ], "dev": true, - "optional": true, - "os": [ - "win32" - ] + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.18.0.tgz", + "integrity": "sha512-Txjh+IxBPbkUB9+SXZMpv+b/vnTEtFyfWZgJ6iyCmt2tdx0OF5WhFowLmnh8ENGNpfUlUZkdI//4IEmhwPieNg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.18.0.tgz", + "integrity": "sha512-UOo5FdvOL0+eIVTgS4tIdbW+TtnBLWg1YBCcU2KWM7nuNwRz9bksDX1bekJJCpu25N1DVWaCwnT39dVQxzqS8g==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@svgr/babel-plugin-add-jsx-attribute": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-8.0.0.tgz", + "integrity": "sha512-b9MIk7yhdS1pMCZM8VeNfUlSKVRhsHZNMl5O9SfaX0l0t5wjdgu4IDzGB8bpnGBBOjGST3rRFVsaaEtI4W6f7g==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-remove-jsx-attribute": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-attribute/-/babel-plugin-remove-jsx-attribute-8.0.0.tgz", + "integrity": "sha512-BcCkm/STipKvbCl6b7QFrMh/vx00vIP63k2eM66MfHJzPr6O2U0jYEViXkHJWqXqQYjdeA9cuCl5KWmlwjDvbA==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-remove-jsx-empty-expression": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-empty-expression/-/babel-plugin-remove-jsx-empty-expression-8.0.0.tgz", + "integrity": "sha512-5BcGCBfBxB5+XSDSWnhTThfI9jcO5f0Ai2V24gZpG+wXF14BzwxxdDb4g6trdOux0rhibGs385BeFMSmxtS3uA==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-replace-jsx-attribute-value": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-replace-jsx-attribute-value/-/babel-plugin-replace-jsx-attribute-value-8.0.0.tgz", + "integrity": "sha512-KVQ+PtIjb1BuYT3ht8M5KbzWBhdAjjUPdlMtpuw/VjT8coTrItWX6Qafl9+ji831JaJcu6PJNKCV0bp01lBNzQ==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-svg-dynamic-title": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-dynamic-title/-/babel-plugin-svg-dynamic-title-8.0.0.tgz", + "integrity": "sha512-omNiKqwjNmOQJ2v6ge4SErBbkooV2aAWwaPFs2vUY7p7GhVkzRkJ00kILXQvRhA6miHnNpXv7MRnnSjdRjK8og==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-svg-em-dimensions": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-em-dimensions/-/babel-plugin-svg-em-dimensions-8.0.0.tgz", + "integrity": "sha512-mURHYnu6Iw3UBTbhGwE/vsngtCIbHE43xCRK7kCw4t01xyGqb2Pd+WXekRRoFOBIY29ZoOhUCTEweDMdrjfi9g==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-transform-react-native-svg": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-react-native-svg/-/babel-plugin-transform-react-native-svg-8.1.0.tgz", + "integrity": "sha512-Tx8T58CHo+7nwJ+EhUwx3LfdNSG9R2OKfaIXXs5soiy5HtgoAEkDay9LIimLOcG8dJQH1wPZp/cnAv6S9CrR1Q==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-transform-svg-component": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-svg-component/-/babel-plugin-transform-svg-component-8.0.0.tgz", + "integrity": "sha512-DFx8xa3cZXTdb/k3kfPeaixecQLgKh5NVBMwD0AQxOzcZawK4oo1Jh9LbrcACUivsCA7TLG8eeWgrDXjTMhRmw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-preset": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-preset/-/babel-preset-8.1.0.tgz", + "integrity": "sha512-7EYDbHE7MxHpv4sxvnVPngw5fuR6pw79SkcrILHJ/iMpuKySNCl5W1qcwPEpU+LgyRXOaAFgH0KhwD18wwg6ug==", + "dev": true, + "dependencies": { + "@svgr/babel-plugin-add-jsx-attribute": "8.0.0", + "@svgr/babel-plugin-remove-jsx-attribute": "8.0.0", + "@svgr/babel-plugin-remove-jsx-empty-expression": "8.0.0", + "@svgr/babel-plugin-replace-jsx-attribute-value": "8.0.0", + "@svgr/babel-plugin-svg-dynamic-title": "8.0.0", + "@svgr/babel-plugin-svg-em-dimensions": "8.0.0", + "@svgr/babel-plugin-transform-react-native-svg": "8.1.0", + "@svgr/babel-plugin-transform-svg-component": "8.0.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/core": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/core/-/core-8.1.0.tgz", + "integrity": "sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==", + "dev": true, + "dependencies": { + "@babel/core": "^7.21.3", + "@svgr/babel-preset": "8.1.0", + "camelcase": "^6.2.0", + "cosmiconfig": "^8.1.3", + "snake-case": "^3.0.4" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.18.0.tgz", - "integrity": "sha512-Txjh+IxBPbkUB9+SXZMpv+b/vnTEtFyfWZgJ6iyCmt2tdx0OF5WhFowLmnh8ENGNpfUlUZkdI//4IEmhwPieNg==", - "cpu": [ - "ia32" - ], + "node_modules/@svgr/hast-util-to-babel-ast": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/hast-util-to-babel-ast/-/hast-util-to-babel-ast-8.0.0.tgz", + "integrity": "sha512-EbDKwO9GpfWP4jN9sGdYwPBU0kdomaPIL2Eu4YwmgP+sJeXT+L7bMwJUBnhzfH8Q2qMBqZ4fJwpCyYsAN3mt2Q==", "dev": true, - "optional": true, - "os": [ - "win32" - ] + "dependencies": { + "@babel/types": "^7.21.3", + "entities": "^4.4.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.18.0.tgz", - "integrity": "sha512-UOo5FdvOL0+eIVTgS4tIdbW+TtnBLWg1YBCcU2KWM7nuNwRz9bksDX1bekJJCpu25N1DVWaCwnT39dVQxzqS8g==", - "cpu": [ - "x64" - ], + "node_modules/@svgr/plugin-jsx": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/plugin-jsx/-/plugin-jsx-8.1.0.tgz", + "integrity": "sha512-0xiIyBsLlr8quN+WyuxooNW9RJ0Dpr8uOnH/xrCVO8GLUcwHISwj1AG0k+LFzteTkAA0GbX0kj9q6Dk70PTiPA==", "dev": true, - "optional": true, - "os": [ - "win32" - ] + "dependencies": { + "@babel/core": "^7.21.3", + "@svgr/babel-preset": "8.1.0", + "@svgr/hast-util-to-babel-ast": "8.0.0", + "svg-parser": "^2.0.4" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@svgr/core": "*" + } + }, + "node_modules/@swc/helpers": { + "version": "0.5.11", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.11.tgz", + "integrity": "sha512-YNlnKRWF2sVojTpIyzwou9XoTNbzbzONwRhOoniEioF1AtaitTvVZblaQRrAzChWQ1bLYyYSWzM18y4WwgzJ+A==", + "dependencies": { + "tslib": "^2.4.0" + } }, "node_modules/@types/babel__core": { "version": "7.20.5", @@ -1237,6 +3980,14 @@ "@babel/types": "^7.20.7" } }, + "node_modules/@types/dompurify": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-3.0.5.tgz", + "integrity": "sha512-1Wg0g3BtQF7sSb27fJQAKck1HECM6zV1EB66j8JH9i3LCjYabJa0FSdiSgsD5K/RbrsR0SiraKacLB+T8ZVYAg==", + "dependencies": { + "@types/trusted-types": "*" + } + }, "node_modules/@types/estree": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", @@ -1247,13 +3998,13 @@ "version": "15.7.12", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz", "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==", - "dev": true + "devOptional": true }, "node_modules/@types/react": { "version": "18.3.3", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.3.tgz", "integrity": "sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==", - "dev": true, + "devOptional": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" @@ -1268,17 +4019,22 @@ "@types/react": "*" } }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==" + }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "7.14.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.14.1.tgz", - "integrity": "sha512-aAJd6bIf2vvQRjUG3ZkNXkmBpN+J7Wd0mfQiiVCJMu9Z5GcZZdcc0j8XwN/BM97Fl7e3SkTXODSk4VehUv7CGw==", + "version": "7.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.13.1.tgz", + "integrity": "sha512-kZqi+WZQaZfPKnsflLJQCz6Ze9FFSMfXrrIOcyargekQxG37ES7DJNpJUE9Q/X5n3yTIP/WPutVNzgknQ7biLg==", "dev": true, "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "7.14.1", - "@typescript-eslint/type-utils": "7.14.1", - "@typescript-eslint/utils": "7.14.1", - "@typescript-eslint/visitor-keys": "7.14.1", + "@typescript-eslint/scope-manager": "7.13.1", + "@typescript-eslint/type-utils": "7.13.1", + "@typescript-eslint/utils": "7.13.1", + "@typescript-eslint/visitor-keys": "7.13.1", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", @@ -1302,15 +4058,15 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "7.14.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.14.1.tgz", - "integrity": "sha512-8lKUOebNLcR0D7RvlcloOacTOWzOqemWEWkKSVpMZVF/XVcwjPR+3MD08QzbW9TCGJ+DwIc6zUSGZ9vd8cO1IA==", + "version": "7.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.13.1.tgz", + "integrity": "sha512-1ELDPlnLvDQ5ybTSrMhRTFDfOQEOXNM+eP+3HT/Yq7ruWpciQw+Avi73pdEbA4SooCawEWo3dtYbF68gN7Ed1A==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "7.14.1", - "@typescript-eslint/types": "7.14.1", - "@typescript-eslint/typescript-estree": "7.14.1", - "@typescript-eslint/visitor-keys": "7.14.1", + "@typescript-eslint/scope-manager": "7.13.1", + "@typescript-eslint/types": "7.13.1", + "@typescript-eslint/typescript-estree": "7.13.1", + "@typescript-eslint/visitor-keys": "7.13.1", "debug": "^4.3.4" }, "engines": { @@ -1330,13 +4086,13 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "7.14.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.14.1.tgz", - "integrity": "sha512-gPrFSsoYcsffYXTOZ+hT7fyJr95rdVe4kGVX1ps/dJ+DfmlnjFN/GcMxXcVkeHDKqsq6uAcVaQaIi3cFffmAbA==", + "version": "7.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.13.1.tgz", + "integrity": "sha512-adbXNVEs6GmbzaCpymHQ0MB6E4TqoiVbC0iqG3uijR8ZYfpAXMGttouQzF4Oat3P2GxDVIrg7bMI/P65LiQZdg==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.14.1", - "@typescript-eslint/visitor-keys": "7.14.1" + "@typescript-eslint/types": "7.13.1", + "@typescript-eslint/visitor-keys": "7.13.1" }, "engines": { "node": "^18.18.0 || >=20.0.0" @@ -1347,13 +4103,13 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "7.14.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.14.1.tgz", - "integrity": "sha512-/MzmgNd3nnbDbOi3LfasXWWe292+iuo+umJ0bCCMCPc1jLO/z2BQmWUUUXvXLbrQey/JgzdF/OV+I5bzEGwJkQ==", + "version": "7.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.13.1.tgz", + "integrity": "sha512-aWDbLu1s9bmgPGXSzNCxELu+0+HQOapV/y+60gPXafR8e2g1Bifxzevaa+4L2ytCWm+CHqpELq4CSoN9ELiwCg==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "7.14.1", - "@typescript-eslint/utils": "7.14.1", + "@typescript-eslint/typescript-estree": "7.13.1", + "@typescript-eslint/utils": "7.13.1", "debug": "^4.3.4", "ts-api-utils": "^1.3.0" }, @@ -1374,9 +4130,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "7.14.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.14.1.tgz", - "integrity": "sha512-mL7zNEOQybo5R3AavY+Am7KLv8BorIv7HCYS5rKoNZKQD9tsfGUpO4KdAn3sSUvTiS4PQkr2+K0KJbxj8H9NDg==", + "version": "7.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.13.1.tgz", + "integrity": "sha512-7K7HMcSQIAND6RBL4kDl24sG/xKM13cA85dc7JnmQXw2cBDngg7c19B++JzvJHRG3zG36n9j1i451GBzRuHchw==", "dev": true, "engines": { "node": "^18.18.0 || >=20.0.0" @@ -1387,13 +4143,13 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "7.14.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.14.1.tgz", - "integrity": "sha512-k5d0VuxViE2ulIO6FbxxSZaxqDVUyMbXcidC8rHvii0I56XZPv8cq+EhMns+d/EVIL41sMXqRbK3D10Oza1bbA==", + "version": "7.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.13.1.tgz", + "integrity": "sha512-uxNr51CMV7npU1BxZzYjoVz9iyjckBduFBP0S5sLlh1tXYzHzgZ3BR9SVsNed+LmwKrmnqN3Kdl5t7eZ5TS1Yw==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.14.1", - "@typescript-eslint/visitor-keys": "7.14.1", + "@typescript-eslint/types": "7.13.1", + "@typescript-eslint/visitor-keys": "7.13.1", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -1415,15 +4171,15 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "7.14.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.14.1.tgz", - "integrity": "sha512-CMmVVELns3nak3cpJhZosDkm63n+DwBlDX8g0k4QUa9BMnF+lH2lr3d130M1Zt1xxmB3LLk3NV7KQCq86ZBBhQ==", + "version": "7.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.13.1.tgz", + "integrity": "sha512-h5MzFBD5a/Gh/fvNdp9pTfqJAbuQC4sCN2WzuXme71lqFJsZtLbjxfSk4r3p02WIArOF9N94pdsLiGutpDbrXQ==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "7.14.1", - "@typescript-eslint/types": "7.14.1", - "@typescript-eslint/typescript-estree": "7.14.1" + "@typescript-eslint/scope-manager": "7.13.1", + "@typescript-eslint/types": "7.13.1", + "@typescript-eslint/typescript-estree": "7.13.1" }, "engines": { "node": "^18.18.0 || >=20.0.0" @@ -1437,12 +4193,12 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "7.14.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.14.1.tgz", - "integrity": "sha512-Crb+F75U1JAEtBeQGxSKwI60hZmmzaqA3z9sYsVm8X7W5cwLEm5bRe0/uXS6+MR/y8CVpKSR/ontIAIEPFcEkA==", + "version": "7.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.13.1.tgz", + "integrity": "sha512-k/Bfne7lrP7hcb7m9zSsgcBmo+8eicqqfNAJ7uUY+jkTFpKeH2FSkWpFRtimBxgkyvqfu9jTPRbYOvud6isdXA==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.14.1", + "@typescript-eslint/types": "7.13.1", "eslint-visitor-keys": "^3.4.3" }, "engines": { @@ -1459,6 +4215,46 @@ "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", "dev": true }, + "node_modules/@vanilla-extract/css": { + "version": "1.15.3", + "resolved": "https://registry.npmjs.org/@vanilla-extract/css/-/css-1.15.3.tgz", + "integrity": "sha512-mxoskDAxdQAspbkmQRxBvolUi1u1jnyy9WZGm+GeH8V2wwhEvndzl1QoK7w8JfA0WFevTxbev5d+i+xACZlPhA==", + "peer": true, + "dependencies": { + "@emotion/hash": "^0.9.0", + "@vanilla-extract/private": "^1.0.5", + "css-what": "^6.1.0", + "cssesc": "^3.0.0", + "csstype": "^3.0.7", + "dedent": "^1.5.3", + "deep-object-diff": "^1.1.9", + "deepmerge": "^4.2.2", + "media-query-parser": "^2.0.2", + "modern-ahocorasick": "^1.0.0", + "picocolors": "^1.0.0" + } + }, + "node_modules/@vanilla-extract/dynamic": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@vanilla-extract/dynamic/-/dynamic-2.1.0.tgz", + "integrity": "sha512-8zl0IgBYRtgD1h+56Zu13wHTiMTJSVEa4F7RWX9vTB/5Xe2KtjoiqApy/szHPVFA56c+ex6A4GpCQjT1bKXbYw==", + "dependencies": { + "@vanilla-extract/private": "^1.0.3" + } + }, + "node_modules/@vanilla-extract/private": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@vanilla-extract/private/-/private-1.0.5.tgz", + "integrity": "sha512-6YXeOEKYTA3UV+RC8DeAjFk+/okoNz/h88R+McnzA2zpaVqTR/Ep+vszkWYlGBcMNO7vEkqbq5nT/JMMvhi+tw==" + }, + "node_modules/@vanilla-extract/recipes": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@vanilla-extract/recipes/-/recipes-0.5.3.tgz", + "integrity": "sha512-SPREq1NmaoKuvJeOV0pppOkwy3pWZUoDufsyQ6iHrbkHhAU7XQqG9o0iZSmg5JoVgDLIiOr9djQb0x9wuxig7A==", + "peerDependencies": { + "@vanilla-extract/css": "^1.0.0" + } + }, "node_modules/@vitejs/plugin-react": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.3.1.tgz", @@ -1499,6 +4295,17 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/agent-base": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -1551,12 +4358,36 @@ "node": ">=8" } }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/brace-expansion": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", @@ -1619,6 +4450,18 @@ "node": ">=6" } }, + "node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001637", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001637.tgz", @@ -1653,6 +4496,43 @@ "node": ">=4" } }, + "node_modules/class-variance-authority": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.0.tgz", + "integrity": "sha512-jFI8IQw4hczaL4ALINxqLEXQbWcNjoSkloa4IaufXCJr6QawJyw7tuRysRsrE8w2p/4gGaxKIt/hX3qz/IbD1A==", + "dependencies": { + "clsx": "2.0.0" + }, + "funding": { + "url": "https://joebell.co.uk" + } + }, + "node_modules/class-variance-authority/node_modules/clsx": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.0.0.tgz", + "integrity": "sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==", + "engines": { + "node": ">=6" + } + }, + "node_modules/classix": { + "version": "2.1.17", + "resolved": "https://registry.npmjs.org/classix/-/classix-2.1.17.tgz", + "integrity": "sha512-0lBXJDoUtHoYHYfHop2BHyE1SehmUZhNfm9i5f3A1qvVTYT1mGLyOJRUZ+uE9QysOtmGnNvy4rrWPja1tIfPSA==" + }, + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==" + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "engines": { + "node": ">=6" + } + }, "node_modules/color-convert": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", @@ -1668,6 +4548,17 @@ "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", "dev": true }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -1680,6 +4571,32 @@ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "dev": true }, + "node_modules/cosmiconfig": { + "version": "8.3.6", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", + "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", + "dev": true, + "dependencies": { + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0", + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -1694,17 +4611,67 @@ "node": ">= 8" } }, + "node_modules/css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", + "peer": true, + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "peer": true, + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cssstyle": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.0.1.tgz", + "integrity": "sha512-8ZYiJ3A/3OkDd093CBT/0UKDWry7ak4BdPTFP2+QEP7cmhouyq/Up709ASSj2cK02BbZiMgk7kYjZNS4QP5qrQ==", + "dependencies": { + "rrweb-cssom": "^0.6.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/cssstyle/node_modules/rrweb-cssom": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.6.0.tgz", + "integrity": "sha512-APM0Gt1KoXBz0iIkkdB/kfvGOwC4UuJFeG/c+yV7wSc7q96cG/kJ0HiYCnzivD9SB53cLV1MlHFNfOuPaadYSw==" + }, "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "dev": true + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" + }, + "node_modules/data-urls": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" + }, + "engines": { + "node": ">=18" + } }, "node_modules/debug": { "version": "4.3.5", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", - "dev": true, "dependencies": { "ms": "2.1.2" }, @@ -1717,12 +4684,54 @@ } } }, + "node_modules/decimal.js": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz", + "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==" + }, + "node_modules/dedent": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.3.tgz", + "integrity": "sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ==", + "peer": true, + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true }, + "node_modules/deep-object-diff": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/deep-object-diff/-/deep-object-diff-1.1.9.tgz", + "integrity": "sha512-Rn+RuwkmkDwCi2/oXOFS9Gsr5lJZu/yTGpK7wAaAIE75CC+LCGEZHpY6VQJa/RoJcrmaA/docWJZvYohlNkWPA==", + "peer": true + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -1747,12 +4756,47 @@ "node": ">=6.0.0" } }, + "node_modules/dompurify": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.1.5.tgz", + "integrity": "sha512-lwG+n5h8QNpxtyrJW/gJWckL+1/DQiYMX8f7t8Z2AZTPw1esVrqjI63i7Zc2Gz0aKzLVMYC1V1PL/ky+aY/NgA==" + }, + "node_modules/dot-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", + "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", + "dev": true, + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, "node_modules/electron-to-chromium": { "version": "1.4.812", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.812.tgz", "integrity": "sha512-7L8fC2Ey/b6SePDFKR2zHAy4mbdp1/38Yk5TsARO66W3hC5KEaeKMMHoxwtuH+jcu2AYLSn9QX04i95t6Fl1Hg==", "dev": true }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, "node_modules/esbuild": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", @@ -2082,6 +5126,12 @@ "node": ">=4.0" } }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true + }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -2206,6 +5256,43 @@ "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", "dev": true }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/framer-motion": { + "version": "11.2.4", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.2.4.tgz", + "integrity": "sha512-D+EXd0lspaZijv3BJhAcSsyGz+gnvoEdnf+QWkPZdhoFzbeX/2skrH9XSVFb0osgUnCajW8x1frjhLuKwa/Reg==", + "dependencies": { + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0", + "react-dom": "^18.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -2319,19 +5406,65 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/graphemer": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "dependencies": { + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz", + "integrity": "sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==", + "dependencies": { + "agent-base": "^7.0.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } }, - "node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, "engines": { - "node": ">=4" + "node": ">=0.10.0" } }, "node_modules/ignore": { @@ -2385,6 +5518,23 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "dev": true }, + "node_modules/intl-messageformat": { + "version": "10.5.14", + "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-10.5.14.tgz", + "integrity": "sha512-IjC6sI0X7YRjjyVH9aUgdftcmZK7WXdHeil4KwbjDnRWjnVitKpAx3rr6t6di1joFp5188VqKcobOPA6mCLG/w==", + "dependencies": { + "@formatjs/ecma402-abstract": "2.0.0", + "@formatjs/fast-memoize": "2.2.0", + "@formatjs/icu-messageformat-parser": "2.7.8", + "tslib": "^2.4.0" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -2424,12 +5574,30 @@ "node": ">=8" } }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==" + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true }, + "node_modules/isomorphic-dompurify": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/isomorphic-dompurify/-/isomorphic-dompurify-2.12.0.tgz", + "integrity": "sha512-jJm6VgJ9toBLqNUHuLudn+2Q3NBBaoPbsh5SzzO2dp9Zq9+p6fEg4Ffuq9RZsofb8OnqE6FJVVq3MRDLlmBHpA==", + "dependencies": { + "@types/dompurify": "^3.0.5", + "dompurify": "^3.1.5", + "jsdom": "^24.1.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -2447,6 +5615,45 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsdom": { + "version": "24.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-24.1.0.tgz", + "integrity": "sha512-6gpM7pRXCwIOKxX47cgOyvyQDN/Eh0f1MeKySBV2xGdKtqJBLj8P25eY3EVCWo2mglDDzozR2r2MW4T+JiNUZA==", + "dependencies": { + "cssstyle": "^4.0.1", + "data-urls": "^5.0.0", + "decimal.js": "^10.4.3", + "form-data": "^4.0.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.4", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.10", + "parse5": "^7.1.2", + "rrweb-cssom": "^0.7.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^4.1.4", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0", + "ws": "^8.17.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "canvas": "^2.11.2" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, "node_modules/jsesc": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", @@ -2465,6 +5672,12 @@ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", "dev": true }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -2498,6 +5711,41 @@ "json-buffer": "3.0.1" } }, + "node_modules/launchdarkly-js-client-sdk": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/launchdarkly-js-client-sdk/-/launchdarkly-js-client-sdk-3.4.0.tgz", + "integrity": "sha512-3v1jKy1RECT0SG/3SGlyRO32vweoNxvWiJuIChRh/Zhk98cHpANuwameXVhwJ4WEDNZJTur73baaKAyatSP46A==", + "dependencies": { + "escape-string-regexp": "^4.0.0", + "launchdarkly-js-sdk-common": "5.3.0" + } + }, + "node_modules/launchdarkly-js-client-sdk/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/launchdarkly-js-sdk-common": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/launchdarkly-js-sdk-common/-/launchdarkly-js-sdk-common-5.3.0.tgz", + "integrity": "sha512-NI5wFF8qhjtpRb4KelGdnwNIOf/boLlbLiseV7uf1jxSeoM/L30DAz87RT8VhYCSGCo4LedGILQFednI/MKFkA==", + "dependencies": { + "base64-js": "^1.3.0", + "fast-deep-equal": "^2.0.1", + "uuid": "^8.0.0" + } + }, + "node_modules/launchdarkly-js-sdk-common/node_modules/fast-deep-equal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", + "integrity": "sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w==" + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -2511,6 +5759,12 @@ "node": ">= 0.8.0" } }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -2543,6 +5797,15 @@ "loose-envify": "cli.js" } }, + "node_modules/lower-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", + "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", + "dev": true, + "dependencies": { + "tslib": "^2.0.3" + } + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -2552,6 +5815,26 @@ "yallist": "^3.0.2" } }, + "node_modules/marked": { + "version": "13.0.1", + "resolved": "https://registry.npmjs.org/marked/-/marked-13.0.1.tgz", + "integrity": "sha512-7kBohS6GrZKvCsNXZyVVXSW7/hGBHe49ng99YPkDCckSUrrG7MSFLCexsRxptzOmyW2eT5dySh4Md1V6my52fA==", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/media-query-parser": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/media-query-parser/-/media-query-parser-2.0.2.tgz", + "integrity": "sha512-1N4qp+jE0pL5Xv4uEcwVUhIkwdUO3S/9gML90nqKA7v7FcOS5vUtatfzok9S9U1EJU8dHWlcv95WLnKmmxZI9w==", + "peer": true, + "dependencies": { + "@babel/runtime": "^7.12.5" + } + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -2574,6 +5857,25 @@ "node": ">=8.6" } }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/minimatch": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", @@ -2589,11 +5891,16 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/modern-ahocorasick": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/modern-ahocorasick/-/modern-ahocorasick-1.0.1.tgz", + "integrity": "sha512-yoe+JbhTClckZ67b2itRtistFKf8yPYelHLc7e5xAwtNAXxM6wJTUx2C7QeVSJFDzKT7bCIFyBVybPMKvmB9AA==", + "peer": true + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "node_modules/nanoid": { "version": "3.3.7", @@ -2619,12 +5926,27 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, + "node_modules/no-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", + "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", + "dev": true, + "dependencies": { + "lower-case": "^2.0.2", + "tslib": "^2.0.3" + } + }, "node_modules/node-releases": { "version": "2.0.14", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", "dev": true }, + "node_modules/nwsapi": { + "version": "2.2.10", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.10.tgz", + "integrity": "sha512-QK0sRs7MKv0tKe1+5uZIQk/C8XGza4DAnztJG8iD+TpJIORARrCxczA738awHrZoHeTjSSoHqao2teO0dC/gFQ==" + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -2693,6 +6015,35 @@ "node": ">=6" } }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse5": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", + "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", + "dependencies": { + "entities": "^4.4.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -2732,8 +6083,7 @@ "node_modules/picocolors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", - "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==", - "dev": true + "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==" }, "node_modules/picomatch": { "version": "2.3.1", @@ -2784,15 +6134,39 @@ "node": ">= 0.8.0" } }, + "node_modules/prettier": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.2.tgz", + "integrity": "sha512-rAVeHYMcv8ATV5d508CFdn+8/pHPpXeIid1DdrPwXnaAdH7cqjVbpJaT5eq4yRAFU/lsbwYwSF/n5iNrdJHPQA==", + "dev": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/psl": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", + "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==" + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, "engines": { "node": ">=6" } }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==" + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -2813,6 +6187,15 @@ } ] }, + "node_modules/rainbow-sprinkles": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/rainbow-sprinkles/-/rainbow-sprinkles-0.17.0.tgz", + "integrity": "sha512-ok3NrylQ0szvJtuBYaB/w09L9zOvvqcSQrvycT2A5XJxQNvwvkeADvTqQWGOQ3b6MkO8UmYccBPt8g8vVvxM9A==", + "peerDependencies": { + "@vanilla-extract/css": "^1", + "@vanilla-extract/dynamic": "^2" + } + }, "node_modules/react": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", @@ -2824,6 +6207,88 @@ "node": ">=0.10.0" } }, + "node_modules/react-aria": { + "version": "3.33.1", + "resolved": "https://registry.npmjs.org/react-aria/-/react-aria-3.33.1.tgz", + "integrity": "sha512-hFC3K/UA+90Krlx2IgRTgzFbC6FSPi4pUwHT+STperPLK+cTEHkI+3Lu0YYwQSBatkgxnIv9+GtFuVbps2kROw==", + "dependencies": { + "@internationalized/string": "^3.2.3", + "@react-aria/breadcrumbs": "^3.5.13", + "@react-aria/button": "^3.9.5", + "@react-aria/calendar": "^3.5.8", + "@react-aria/checkbox": "^3.14.3", + "@react-aria/combobox": "^3.9.1", + "@react-aria/datepicker": "^3.10.1", + "@react-aria/dialog": "^3.5.14", + "@react-aria/dnd": "^3.6.1", + "@react-aria/focus": "^3.17.1", + "@react-aria/gridlist": "^3.8.1", + "@react-aria/i18n": "^3.11.1", + "@react-aria/interactions": "^3.21.3", + "@react-aria/label": "^3.7.8", + "@react-aria/link": "^3.7.1", + "@react-aria/listbox": "^3.12.1", + "@react-aria/menu": "^3.14.1", + "@react-aria/meter": "^3.4.13", + "@react-aria/numberfield": "^3.11.3", + "@react-aria/overlays": "^3.22.1", + "@react-aria/progress": "^3.4.13", + "@react-aria/radio": "^3.10.4", + "@react-aria/searchfield": "^3.7.5", + "@react-aria/select": "^3.14.5", + "@react-aria/selection": "^3.18.1", + "@react-aria/separator": "^3.3.13", + "@react-aria/slider": "^3.7.8", + "@react-aria/ssr": "^3.9.4", + "@react-aria/switch": "^3.6.4", + "@react-aria/table": "^3.14.1", + "@react-aria/tabs": "^3.9.1", + "@react-aria/tag": "^3.4.1", + "@react-aria/textfield": "^3.14.5", + "@react-aria/tooltip": "^3.7.4", + "@react-aria/utils": "^3.24.1", + "@react-aria/visually-hidden": "^3.8.12", + "@react-types/shared": "^3.23.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/react-aria-components": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/react-aria-components/-/react-aria-components-1.2.1.tgz", + "integrity": "sha512-iGIdDjbTyLLn0/tGUyBQxxu+E1bw4/H4AU89d0cRcu8yIdw6MXG29YElmRHn0ugiyrERrk/YQALihstnns5kRQ==", + "dependencies": { + "@internationalized/date": "^3.5.4", + "@internationalized/string": "^3.2.3", + "@react-aria/color": "3.0.0-beta.33", + "@react-aria/focus": "^3.17.1", + "@react-aria/interactions": "^3.21.3", + "@react-aria/menu": "^3.14.1", + "@react-aria/toolbar": "3.0.0-beta.5", + "@react-aria/tree": "3.0.0-alpha.1", + "@react-aria/utils": "^3.24.1", + "@react-stately/color": "^3.6.1", + "@react-stately/menu": "^3.7.1", + "@react-stately/table": "^3.11.8", + "@react-stately/utils": "^3.10.1", + "@react-types/color": "3.0.0-beta.25", + "@react-types/form": "^3.7.4", + "@react-types/grid": "^3.2.6", + "@react-types/shared": "^3.23.1", + "@react-types/table": "^3.9.5", + "@swc/helpers": "^0.5.0", + "client-only": "^0.0.1", + "react-aria": "^3.33.1", + "react-stately": "^3.31.1", + "use-sync-external-store": "^1.2.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, "node_modules/react-dom": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", @@ -2845,6 +6310,93 @@ "node": ">=0.10.0" } }, + "node_modules/react-router": { + "version": "6.16.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.16.0.tgz", + "integrity": "sha512-VT4Mmc4jj5YyjpOi5jOf0I+TYzGpvzERy4ckNSvSh2RArv8LLoCxlsZ2D+tc7zgjxcY34oTz2hZaeX5RVprKqA==", + "dependencies": { + "@remix-run/router": "1.9.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.16.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.16.0.tgz", + "integrity": "sha512-aTfBLv3mk/gaKLxgRDUPbPw+s4Y/O+ma3rEN1u8EgEpLpPe6gNjIsWt9rxushMHHMb7mSwxRGdGlGdvmFsyPIg==", + "dependencies": { + "@remix-run/router": "1.9.0", + "react-router": "6.16.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/react-stately": { + "version": "3.31.1", + "resolved": "https://registry.npmjs.org/react-stately/-/react-stately-3.31.1.tgz", + "integrity": "sha512-wuq673NHkYSdoceGryjtMJJvB9iQgyDkQDsnTN0t2v91pXjGDsN/EcOvnUrxXSBtY9eLdIw74R54z9GX5cJNEg==", + "dependencies": { + "@react-stately/calendar": "^3.5.1", + "@react-stately/checkbox": "^3.6.5", + "@react-stately/collections": "^3.10.7", + "@react-stately/combobox": "^3.8.4", + "@react-stately/data": "^3.11.4", + "@react-stately/datepicker": "^3.9.4", + "@react-stately/dnd": "^3.3.1", + "@react-stately/form": "^3.0.3", + "@react-stately/list": "^3.10.5", + "@react-stately/menu": "^3.7.1", + "@react-stately/numberfield": "^3.9.3", + "@react-stately/overlays": "^3.6.7", + "@react-stately/radio": "^3.10.4", + "@react-stately/searchfield": "^3.5.3", + "@react-stately/select": "^3.6.4", + "@react-stately/selection": "^3.15.1", + "@react-stately/slider": "^3.5.4", + "@react-stately/table": "^3.11.8", + "@react-stately/tabs": "^3.6.6", + "@react-stately/toggle": "^3.7.4", + "@react-stately/tooltip": "^3.4.9", + "@react-stately/tree": "^3.8.1", + "@react-types/shared": "^3.23.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/react-virtual": { + "version": "2.10.4", + "resolved": "https://registry.npmjs.org/react-virtual/-/react-virtual-2.10.4.tgz", + "integrity": "sha512-Ir6+oPQZTVHfa6+JL9M7cvMILstFZH/H3jqeYeKI4MSUX+rIruVwFC6nGVXw9wqAw8L0Kg2KvfXxI85OvYQdpQ==", + "funding": [ + "https://github.com/sponsors/tannerlinsley" + ], + "dependencies": { + "@reach/observe-rect": "^1.1.0" + }, + "peerDependencies": { + "react": "^16.6.3 || ^17.0.0" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -2915,6 +6467,11 @@ "fsevents": "~2.3.2" } }, + "node_modules/rrweb-cssom": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.7.1.tgz", + "integrity": "sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==" + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -2938,6 +6495,22 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, "node_modules/scheduler": { "version": "0.23.2", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", @@ -2988,6 +6561,16 @@ "node": ">=8" } }, + "node_modules/snake-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/snake-case/-/snake-case-3.0.4.tgz", + "integrity": "sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==", + "dev": true, + "dependencies": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, "node_modules/source-map-js": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", @@ -3033,6 +6616,17 @@ "node": ">=4" } }, + "node_modules/svg-parser": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/svg-parser/-/svg-parser-2.0.4.tgz", + "integrity": "sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==", + "dev": true + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==" + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -3060,6 +6654,31 @@ "node": ">=8.0" } }, + "node_modules/tough-cookie": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", + "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", + "dependencies": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tr46": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.0.0.tgz", + "integrity": "sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/ts-api-utils": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", @@ -3072,6 +6691,11 @@ "typescript": ">=4.2.0" } }, + "node_modules/tslib": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", + "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==" + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -3097,9 +6721,9 @@ } }, "node_modules/typescript": { - "version": "5.5.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.2.tgz", - "integrity": "sha512-NcRtPEOsPFFWjobJEtfihkLCZCXZt/os3zf8nTxjVH3RvTSxjrCamJpbExGvYOF+tFHc3pA65qpdwPbzjohhew==", + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", + "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", "dev": true, "bin": { "tsc": "bin/tsc", @@ -3109,6 +6733,14 @@ "node": ">=14.17" } }, + "node_modules/universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "engines": { + "node": ">= 4.0.0" + } + }, "node_modules/update-browserslist-db": { "version": "1.0.16", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.16.tgz", @@ -3148,6 +6780,31 @@ "punycode": "^2.1.0" } }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, + "node_modules/use-sync-external-store": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.2.tgz", + "integrity": "sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/vite": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/vite/-/vite-5.3.1.tgz", @@ -3203,6 +6860,70 @@ } } }, + "node_modules/vite-plugin-svgr": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/vite-plugin-svgr/-/vite-plugin-svgr-4.2.0.tgz", + "integrity": "sha512-SC7+FfVtNQk7So0XMjrrtLAbEC8qjFPifyD7+fs/E6aaNdVde6umlVVh0QuwDLdOMu7vp5RiGFsB70nj5yo0XA==", + "dev": true, + "dependencies": { + "@rollup/pluginutils": "^5.0.5", + "@svgr/core": "^8.1.0", + "@svgr/plugin-jsx": "^8.1.0" + }, + "peerDependencies": { + "vite": "^2.6.0 || 3 || 4 || 5" + } + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-url": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.0.0.tgz", + "integrity": "sha512-1lfMEm2IEr7RIV+f4lUNPOqfFL+pO+Xw3fJSqmjX9AbXcXcYOkCe1P6+9VBZB6n94af16NfZf+sSk0JCBZC9aw==", + "dependencies": { + "tr46": "^5.0.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -3233,6 +6954,39 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "dev": true }, + "node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==" + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", diff --git a/internal/dev_server/ui/package.json b/internal/dev_server/ui/package.json index b28fd35d..9aa4763f 100644 --- a/internal/dev_server/ui/package.json +++ b/internal/dev_server/ui/package.json @@ -7,22 +7,30 @@ "dev": "vite", "build": "tsc -b && vite build", "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", + "prettier:write": "prettier . --write", "preview": "vite preview" }, "dependencies": { - "react": "^18.3.1", - "react-dom": "^18.3.1" + "@launchpad-ui/components": "0.2.12", + "@launchpad-ui/core": "0.49.22", + "@launchpad-ui/icons": "0.18.4", + "@launchpad-ui/tokens": "0.9.12", + "launchdarkly-js-client-sdk": "3.4.0", + "react": "18.3.1", + "react-dom": "18.3.1" }, "devDependencies": { - "@types/react": "^18.3.3", - "@types/react-dom": "^18.3.0", - "@typescript-eslint/eslint-plugin": "^7.13.1", - "@typescript-eslint/parser": "^7.13.1", - "@vitejs/plugin-react": "^4.3.1", - "eslint": "^8.57.0", - "eslint-plugin-react-hooks": "^4.6.2", - "eslint-plugin-react-refresh": "^0.4.7", - "typescript": "^5.2.2", - "vite": "^5.3.1" + "@types/react": "18.3.3", + "@types/react-dom": "18.3.0", + "@typescript-eslint/eslint-plugin": "7.13.1", + "@typescript-eslint/parser": "7.13.1", + "@vitejs/plugin-react": "4.3.1", + "eslint": "8.57.0", + "eslint-plugin-react-hooks": "4.6.2", + "eslint-plugin-react-refresh": "0.4.7", + "prettier": "3.3.2", + "typescript": "5.2.2", + "vite": "5.3.1", + "vite-plugin-svgr": "^4.2.0" } } diff --git a/internal/dev_server/ui/src/App.css b/internal/dev_server/ui/src/App.css index b9d355df..6909e21c 100644 --- a/internal/dev_server/ui/src/App.css +++ b/internal/dev_server/ui/src/App.css @@ -1,42 +1,31 @@ -#root { - max-width: 1280px; - margin: 0 auto; - padding: 2rem; - text-align: center; -} - -.logo { - height: 6em; - padding: 1.5em; - will-change: filter; - transition: filter 300ms; -} -.logo:hover { - filter: drop-shadow(0 0 2em #646cffaa); -} -.logo.react:hover { - filter: drop-shadow(0 0 2em #61dafbaa); -} +@import url("../node_modules/@launchpad-ui/tokens/dist/index.css"); +@import url("../node_modules/@launchpad-ui/tokens/dist/media-queries.css"); +@import url("../node_modules/@launchpad-ui/tokens/dist/themes.css"); -@keyframes logo-spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } +html, +body, +#root { + height: 100%; + width: 100%; + box-sizing: border-box; } -@media (prefers-reduced-motion: no-preference) { - a:nth-of-type(2) .logo { - animation: logo-spin infinite 20s linear; - } +#root { + padding: 2rem; } -.card { - padding: 2em; +ul.flags-list { + margin: 0; + padding: 0; + max-width: 40rem; + margin: 0 auto; } -.read-the-docs { - color: #888; +ul.flags-list li { + list-style-type: none; + padding: 0.5rem 0; + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; } diff --git a/internal/dev_server/ui/src/App.tsx b/internal/dev_server/ui/src/App.tsx index 1f554812..ea3d84dc 100644 --- a/internal/dev_server/ui/src/App.tsx +++ b/internal/dev_server/ui/src/App.tsx @@ -1,35 +1,82 @@ -import { useState } from 'react' -import reactLogo from './assets/react.svg' -import viteLogo from '/vite.svg' -import './App.css' +import { LDFlagSet, LDFlagValue } from "launchdarkly-js-client-sdk"; +import { Switch, TextField } from "@launchpad-ui/components"; +import { CopyToClipboard, InlineEdit } from "@launchpad-ui/core"; +import "./App.css"; function App() { - const [count, setCount] = useState(0) + // TODO: replace these with actual flags when we wire up the APIs + const mockFlags: LDFlagSet = { + "flag-b": 10, + "flag-a": true, + "flag-c": { + propA: "A", + }, + }; + + const sortedFlags = Object.keys(mockFlags) + .sort((a, b) => a.localeCompare(b)) + .reduce>((accum, flagKey) => { + accum[flagKey] = mockFlags[flagKey]; + + return accum; + }, {}); return ( <> -

Vite + React

-
- -

- Edit src/App.tsx and save to test HMR -

+
    + {Object.entries(sortedFlags).map(([flagKey, flagValue]) => { + let valueNode; + + if (typeof flagValue === "boolean") { + valueNode = ( + { + // todo + }} + /> + ); + } else if (typeof flagValue === "number") { + valueNode = ( + { + // todo + }} + /> + ); + } else { + valueNode = ( + { + // todo + }} + renderInput={} + > + + {JSON.stringify(flagValue)} + + + ); + } + + return ( +
  • + {flagKey} +
    {valueNode}
    +
  • + ); + })} +
-

- Click on the Vite and React logos to learn more -

- ) + ); } -export default App +export default App; diff --git a/internal/dev_server/ui/src/IconProvider.tsx b/internal/dev_server/ui/src/IconProvider.tsx new file mode 100644 index 00000000..33133068 --- /dev/null +++ b/internal/dev_server/ui/src/IconProvider.tsx @@ -0,0 +1,28 @@ +import type { ReactNode } from "react"; +import { useEffect } from "react"; +import { IconContext } from "@launchpad-ui/icons"; +import sprite from "@launchpad-ui/icons/sprite.svg"; + +export function IconProvider({ children }: { children: ReactNode }) { + useEffect(() => { + fetch(sprite) + .then(async (response) => response.text()) + .then((data) => { + const parser = new DOMParser(); + const content = parser.parseFromString( + data, + "image/svg+xml", + ).documentElement; + content.id = "lp-icons-sprite"; + content.style.display = "none"; + document.body.appendChild(content); + }) + .catch((_err) => { + // :shrug: + }); + }, []); + + return ( + {children} + ); +} diff --git a/internal/dev_server/ui/src/assets/react.svg b/internal/dev_server/ui/src/assets/react.svg deleted file mode 100644 index 6c87de9b..00000000 --- a/internal/dev_server/ui/src/assets/react.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/internal/dev_server/ui/src/index.css b/internal/dev_server/ui/src/index.css deleted file mode 100644 index 6119ad9a..00000000 --- a/internal/dev_server/ui/src/index.css +++ /dev/null @@ -1,68 +0,0 @@ -:root { - font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; - line-height: 1.5; - font-weight: 400; - - color-scheme: light dark; - color: rgba(255, 255, 255, 0.87); - background-color: #242424; - - font-synthesis: none; - text-rendering: optimizeLegibility; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -a { - font-weight: 500; - color: #646cff; - text-decoration: inherit; -} -a:hover { - color: #535bf2; -} - -body { - margin: 0; - display: flex; - place-items: center; - min-width: 320px; - min-height: 100vh; -} - -h1 { - font-size: 3.2em; - line-height: 1.1; -} - -button { - border-radius: 8px; - border: 1px solid transparent; - padding: 0.6em 1.2em; - font-size: 1em; - font-weight: 500; - font-family: inherit; - background-color: #1a1a1a; - cursor: pointer; - transition: border-color 0.25s; -} -button:hover { - border-color: #646cff; -} -button:focus, -button:focus-visible { - outline: 4px auto -webkit-focus-ring-color; -} - -@media (prefers-color-scheme: light) { - :root { - color: #213547; - background-color: #ffffff; - } - a:hover { - color: #747bff; - } - button { - background-color: #f9f9f9; - } -} diff --git a/internal/dev_server/ui/src/main.tsx b/internal/dev_server/ui/src/main.tsx index 3d7150da..7d268e02 100644 --- a/internal/dev_server/ui/src/main.tsx +++ b/internal/dev_server/ui/src/main.tsx @@ -1,10 +1,12 @@ -import React from 'react' -import ReactDOM from 'react-dom/client' -import App from './App.tsx' -import './index.css' +import React from "react"; +import ReactDOM from "react-dom/client"; +import App from "./App.tsx"; +import { IconProvider } from "./IconProvider.tsx"; -ReactDOM.createRoot(document.getElementById('root')!).render( +ReactDOM.createRoot(document.getElementById("root")!).render( - + + + , -) +); diff --git a/internal/dev_server/ui/src/vite-env.d.ts b/internal/dev_server/ui/src/vite-env.d.ts index 11f02fe2..b1f45c78 100644 --- a/internal/dev_server/ui/src/vite-env.d.ts +++ b/internal/dev_server/ui/src/vite-env.d.ts @@ -1 +1,2 @@ /// +/// diff --git a/internal/dev_server/ui/vite.config.ts b/internal/dev_server/ui/vite.config.ts index 5a33944a..ba3d0846 100644 --- a/internal/dev_server/ui/vite.config.ts +++ b/internal/dev_server/ui/vite.config.ts @@ -1,7 +1,8 @@ -import { defineConfig } from 'vite' -import react from '@vitejs/plugin-react' +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; +import svgr from "vite-plugin-svgr"; // https://vitejs.dev/config/ export default defineConfig({ - plugins: [react()], -}) + plugins: [react(), svgr()], +}); From 3f3534e0a0027a247b52b56912771b39bc6c07f7 Mon Sep 17 00:00:00 2001 From: Keegan Watkins <109103953+kwatkins-ld@users.noreply.github.com> Date: Wed, 26 Jun 2024 17:12:56 -0500 Subject: [PATCH 35/79] feat: remove mock flags in devserver UI (#340) Remove mock flags in devserver UI --- internal/dev_server/ui/src/App.css | 9 ++ internal/dev_server/ui/src/App.tsx | 132 ++++++++++++++------------ internal/dev_server/ui/vite.config.ts | 9 ++ 3 files changed, 89 insertions(+), 61 deletions(-) diff --git a/internal/dev_server/ui/src/App.css b/internal/dev_server/ui/src/App.css index 6909e21c..0d0dd6ec 100644 --- a/internal/dev_server/ui/src/App.css +++ b/internal/dev_server/ui/src/App.css @@ -29,3 +29,12 @@ ul.flags-list li { justify-content: space-between; align-items: center; } + +code { + background-color: var(--lp-color-bg-ui-secondary); + border: 1px solid var(--lp-color-border-ui-primary); + border-radius: 2px; + color: var(--lp-color-pink-600); + font-size: 87.5%; + padding: 0.125rem 0.25rem; +} diff --git a/internal/dev_server/ui/src/App.tsx b/internal/dev_server/ui/src/App.tsx index ea3d84dc..12110669 100644 --- a/internal/dev_server/ui/src/App.tsx +++ b/internal/dev_server/ui/src/App.tsx @@ -1,78 +1,88 @@ import { LDFlagSet, LDFlagValue } from "launchdarkly-js-client-sdk"; -import { Switch, TextField } from "@launchpad-ui/components"; -import { CopyToClipboard, InlineEdit } from "@launchpad-ui/core"; +import { Switch } from "@launchpad-ui/components"; +import { CopyToClipboard, InlineEdit, TextField } from "@launchpad-ui/core"; import "./App.css"; +import { useEffect, useState } from "react"; function App() { - // TODO: replace these with actual flags when we wire up the APIs - const mockFlags: LDFlagSet = { - "flag-b": 10, - "flag-a": true, - "flag-c": { - propA: "A", - }, - }; + const [flags, setFlags] = useState(null); - const sortedFlags = Object.keys(mockFlags) - .sort((a, b) => a.localeCompare(b)) - .reduce>((accum, flagKey) => { - accum[flagKey] = mockFlags[flagKey]; + useEffect(() => { + fetch("/api/dev/projects/default?expand=overrides") + .then(async (res) => { + if (res.ok) { + const flags = (await res.json()).flagsState; + const sortedFlags = Object.keys(flags) + .sort((a, b) => a.localeCompare(b)) + .reduce>((accum, flagKey) => { + accum[flagKey] = flags[flagKey]; - return accum; - }, {}); + return accum; + }, {}); + + setFlags(sortedFlags); + } + }) + .catch((e) => { + // todo: handle failure to load flag data + }); + }, []); return ( <>
    - {Object.entries(sortedFlags).map(([flagKey, flagValue]) => { - let valueNode; + {flags && + Object.entries(flags).map(([flagKey, { value: flagValue }]) => { + let valueNode; - if (typeof flagValue === "boolean") { - valueNode = ( - { - // todo - }} - /> - ); - } else if (typeof flagValue === "number") { - valueNode = ( - { - // todo - }} - /> - ); - } else { - valueNode = ( - { - // todo - }} - renderInput={} - > - { + // todo + }} + /> + ); + } else if (typeof flagValue === "number") { + valueNode = ( + { + // todo + }} + /> + ); + } else { + valueNode = ( + { + // todo + }} + renderInput={} > - {JSON.stringify(flagValue)} - - - ); - } + + {JSON.stringify(flagValue)} + + + ); + } - return ( -
  • - {flagKey} -
    {valueNode}
    -
  • - ); - })} + return ( +
  • + + {flagKey} + +
    {valueNode}
    +
  • + ); + })}
diff --git a/internal/dev_server/ui/vite.config.ts b/internal/dev_server/ui/vite.config.ts index ba3d0846..802570f0 100644 --- a/internal/dev_server/ui/vite.config.ts +++ b/internal/dev_server/ui/vite.config.ts @@ -5,4 +5,13 @@ import svgr from "vite-plugin-svgr"; // https://vitejs.dev/config/ export default defineConfig({ plugins: [react(), svgr()], + server: { + proxy: { + "/api": { + target: "http://localhost:8765", + changeOrigin: true, + rewrite: (path: string) => path.replace(/^\/api/, ""), + }, + }, + }, }); From 67b6c5e5ee296f3e659e0e17d84085dfff290cab Mon Sep 17 00:00:00 2001 From: Kerrie Martinez Date: Wed, 26 Jun 2024 15:13:38 -0700 Subject: [PATCH 36/79] Add overrides to projects (#341) * adding overrides expand to the api * adding overrides to project response * removing comment --- internal/dev_server/api/api.yaml | 74 ++++++++++++++++---------- internal/dev_server/api/server.gen.go | 76 +++++++++++++++++---------- internal/dev_server/api/server.go | 27 +++++++--- internal/dev_server/db/sqlite.go | 45 ++++++++++++++++ internal/dev_server/model/store.go | 1 + 5 files changed, 160 insertions(+), 63 deletions(-) diff --git a/internal/dev_server/api/api.yaml b/internal/dev_server/api/api.yaml index 834841da..df162564 100644 --- a/internal/dev_server/api/api.yaml +++ b/internal/dev_server/api/api.yaml @@ -28,6 +28,15 @@ paths: summary: get the specified project and its configuration for syncing from the LaunchDarkly Service parameters: - $ref: "#/components/parameters/projectKey" + - name: expand + description: Available expand options for this endpoint. + in: query + schema: + type: array + items: + type: string + enum: + - overrides responses: 200: $ref: "#/components/responses/Project" @@ -139,6 +148,42 @@ components: x-go-type: ldvalue.Value x-go-type-import: path: github.com/launchdarkly/go-sdk-common/v3/ldvalue + Project: + description: Project + type: object + required: + - sourceEnvironmentKey + - context + - _lastSyncedFromSource + properties: + sourceEnvironmentKey: + type: string + description: environment to copy flag values from + context: + type: object + description: context object to use when evaluating flags in source environment + default: + key: "local-dev" + kind: user + x-go-type: ldcontext.Context + x-go-type-import: + path: github.com/launchdarkly/go-sdk-common/v3/ldcontext + flagsState: + type: object + description: flags and their values and version for a given project in the source environment + x-go-type: model.FlagsState + x-go-type-import: + path: github.com/launchdarkly/ldcli/internal/dev_server/model + overrides: + type: object + description: flags and their values and version for a given project in the source environment + x-go-type: model.FlagsState + x-go-type-import: + path: github.com/launchdarkly/ldcli/internal/dev_server/model + _lastSyncedFromSource: + type: integer + x-go-type: int64 + description: unix timestamp for the lat time the flag values were synced from the source environment responses: FlagOverride: description: Flag override @@ -160,31 +205,4 @@ components: content: application/json: schema: - type: object - required: - - sourceEnvironmentKey - - context - - _lastSyncedFromSource - properties: - sourceEnvironmentKey: - type: string - description: environment to copy flag values from - context: - type: object - description: context object to use when evaluating flags in source environment - default: - key: "local-dev" - kind: user - x-go-type: ldcontext.Context - x-go-type-import: - path: github.com/launchdarkly/go-sdk-common/v3/ldcontext - flagsState: - type: object - description: flags and their values and version for a given project in the source environment - x-go-type: model.FlagsState - x-go-type-import: - path: github.com/launchdarkly/ldcli/internal/dev_server/model - _lastSyncedFromSource: - type: integer - x-go-type: int64 - description: unix timestamp for the lat time the flag values were synced from the source environment + $ref: "#/components/schemas/Project" diff --git a/internal/dev_server/api/server.gen.go b/internal/dev_server/api/server.gen.go index 293605a7..b1d0da1b 100644 --- a/internal/dev_server/api/server.gen.go +++ b/internal/dev_server/api/server.gen.go @@ -17,9 +17,32 @@ import ( strictnethttp "github.com/oapi-codegen/runtime/strictmiddleware/nethttp" ) +// Defines values for GetDevProjectsProjectKeyParamsExpand. +const ( + Overrides GetDevProjectsProjectKeyParamsExpand = "overrides" +) + // FlagValue value of a feature flag variation type FlagValue = ldvalue.Value +// Project Project +type Project struct { + // LastSyncedFromSource unix timestamp for the lat time the flag values were synced from the source environment + LastSyncedFromSource int64 `json:"_lastSyncedFromSource"` + + // Context context object to use when evaluating flags in source environment + Context ldcontext.Context `json:"context"` + + // FlagsState flags and their values and version for a given project in the source environment + FlagsState *model.FlagsState `json:"flagsState,omitempty"` + + // Overrides flags and their values and version for a given project in the source environment + Overrides *model.FlagsState `json:"overrides,omitempty"` + + // SourceEnvironmentKey environment to copy flag values from + SourceEnvironmentKey string `json:"sourceEnvironmentKey"` +} + // FlagKey defines model for flagKey. type FlagKey = string @@ -35,21 +58,15 @@ type FlagOverride struct { Value FlagValue `json:"value"` } -// Project defines model for Project. -type Project struct { - // LastSyncedFromSource unix timestamp for the lat time the flag values were synced from the source environment - LastSyncedFromSource int64 `json:"_lastSyncedFromSource"` - - // Context context object to use when evaluating flags in source environment - Context ldcontext.Context `json:"context"` - - // FlagsState flags and their values and version for a given project in the source environment - FlagsState *model.FlagsState `json:"flagsState,omitempty"` - - // SourceEnvironmentKey environment to copy flag values from - SourceEnvironmentKey string `json:"sourceEnvironmentKey"` +// GetDevProjectsProjectKeyParams defines parameters for GetDevProjectsProjectKey. +type GetDevProjectsProjectKeyParams struct { + // Expand Available expand options for this endpoint. + Expand *[]GetDevProjectsProjectKeyParamsExpand `form:"expand,omitempty" json:"expand,omitempty"` } +// GetDevProjectsProjectKeyParamsExpand defines parameters for GetDevProjectsProjectKey. +type GetDevProjectsProjectKeyParamsExpand string + // PostDevProjectsProjectKeyJSONBody defines parameters for PostDevProjectsProjectKey. type PostDevProjectsProjectKeyJSONBody struct { // SourceEnvironmentKey environment to copy flag values from @@ -72,7 +89,7 @@ type ServerInterface interface { DeleteDevProjectsProjectKey(w http.ResponseWriter, r *http.Request, projectKey ProjectKey) // get the specified project and its configuration for syncing from the LaunchDarkly Service // (GET /dev/projects/{projectKey}) - GetDevProjectsProjectKey(w http.ResponseWriter, r *http.Request, projectKey ProjectKey) + GetDevProjectsProjectKey(w http.ResponseWriter, r *http.Request, projectKey ProjectKey, params GetDevProjectsProjectKeyParams) // Add the project to the dev server // (POST /dev/projects/{projectKey}) PostDevProjectsProjectKey(w http.ResponseWriter, r *http.Request, projectKey ProjectKey) @@ -149,8 +166,19 @@ func (siw *ServerInterfaceWrapper) GetDevProjectsProjectKey(w http.ResponseWrite return } + // Parameter object where we will unmarshal all parameters from the context + var params GetDevProjectsProjectKeyParams + + // ------------- Optional query parameter "expand" ------------- + + err = runtime.BindQueryParameter("form", true, false, "expand", r.URL.Query(), ¶ms.Expand) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "expand", Err: err}) + return + } + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - siw.Handler.GetDevProjectsProjectKey(w, r, projectKey) + siw.Handler.GetDevProjectsProjectKey(w, r, projectKey, params) })) for _, middleware := range siw.HandlerMiddlewares { @@ -392,19 +420,7 @@ type FlagOverrideJSONResponse struct { Value FlagValue `json:"value"` } -type ProjectJSONResponse struct { - // LastSyncedFromSource unix timestamp for the lat time the flag values were synced from the source environment - LastSyncedFromSource int64 `json:"_lastSyncedFromSource"` - - // Context context object to use when evaluating flags in source environment - Context ldcontext.Context `json:"context"` - - // FlagsState flags and their values and version for a given project in the source environment - FlagsState *model.FlagsState `json:"flagsState,omitempty"` - - // SourceEnvironmentKey environment to copy flag values from - SourceEnvironmentKey string `json:"sourceEnvironmentKey"` -} +type ProjectJSONResponse Project type GetDevProjectsRequestObject struct { } @@ -448,6 +464,7 @@ func (response DeleteDevProjectsProjectKey404Response) VisitDeleteDevProjectsPro type GetDevProjectsProjectKeyRequestObject struct { ProjectKey ProjectKey `json:"projectKey"` + Params GetDevProjectsProjectKeyParams } type GetDevProjectsProjectKeyResponseObject interface { @@ -635,10 +652,11 @@ func (sh *strictHandler) DeleteDevProjectsProjectKey(w http.ResponseWriter, r *h } // GetDevProjectsProjectKey operation middleware -func (sh *strictHandler) GetDevProjectsProjectKey(w http.ResponseWriter, r *http.Request, projectKey ProjectKey) { +func (sh *strictHandler) GetDevProjectsProjectKey(w http.ResponseWriter, r *http.Request, projectKey ProjectKey, params GetDevProjectsProjectKeyParams) { var request GetDevProjectsProjectKeyRequestObject request.ProjectKey = projectKey + request.Params = params handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { return sh.ssi.GetDevProjectsProjectKey(ctx, request.(GetDevProjectsProjectKeyRequestObject)) diff --git a/internal/dev_server/api/server.go b/internal/dev_server/api/server.go index fc78d142..5f43e17d 100644 --- a/internal/dev_server/api/server.go +++ b/internal/dev_server/api/server.go @@ -49,13 +49,28 @@ func (s Server) GetDevProjectsProjectKey(ctx context.Context, request GetDevProj return GetDevProjectsProjectKey404Response{}, nil } + response := ProjectJSONResponse{ + LastSyncedFromSource: project.LastSyncTime.Unix(), + Context: project.Context, + SourceEnvironmentKey: project.SourceEnvironmentKey, + FlagsState: &project.FlagState, + } + + if request.Params.Expand != nil { + for _, item := range *request.Params.Expand { + if item == "overrides" { + overrides, err := store.GetOverridesForProject(ctx, request.ProjectKey) + if err != nil { + return nil, err + } + response.Overrides = &overrides + } + } + + } + return GetDevProjectsProjectKey200JSONResponse{ - ProjectJSONResponse{ - LastSyncedFromSource: project.LastSyncTime.Unix(), - Context: project.Context, - SourceEnvironmentKey: project.SourceEnvironmentKey, - FlagsState: &project.FlagState, - }, + response, }, nil } diff --git a/internal/dev_server/db/sqlite.go b/internal/dev_server/db/sqlite.go index b1f5022c..a3cdcda4 100644 --- a/internal/dev_server/db/sqlite.go +++ b/internal/dev_server/db/sqlite.go @@ -8,6 +8,7 @@ import ( _ "github.com/mattn/go-sqlite3" "github.com/pkg/errors" + "github.com/launchdarkly/go-sdk-common/v3/ldvalue" "github.com/launchdarkly/ldcli/internal/dev_server/model" ) @@ -99,6 +100,50 @@ VALUES (?, ?, ?, ?, ?) return err } +func (s Sqlite) GetOverridesForProject(ctx context.Context, projectKey string) (model.FlagsState, error) { + rows, err := s.database.QueryContext(ctx, ` + SELECT flag_key, value, version + FROM overrides + WHERE project_key = ? AND active=1 + `, projectKey) + + if err != nil { + return nil, err + } + defer rows.Close() + + overrides := make(model.FlagsState) + for rows.Next() { + var flagKey string + var value string + var version int + var flagState model.FlagState + + err = rows.Scan(&flagKey, &value, &version) + if err != nil { + return nil, err + } + + var ldValue ldvalue.Value + err = json.Unmarshal([]byte(value), &ldValue) + if err != nil { + return nil, err + } + + flagState = model.FlagState{ + Value: ldValue, + Version: version, + } + overrides[flagKey] = flagState + } + + if err = rows.Err(); err != nil { + return nil, err + } + + return overrides, nil +} + func (s Sqlite) UpsertOverride(ctx context.Context, override model.Override) error { valueJson, err := override.Value.MarshalJSON() if err != nil { diff --git a/internal/dev_server/model/store.go b/internal/dev_server/model/store.go index 4440c2be..db706daf 100644 --- a/internal/dev_server/model/store.go +++ b/internal/dev_server/model/store.go @@ -18,6 +18,7 @@ type Store interface { DeleteDevProject(ctx context.Context, projectKey string) (bool, error) InsertProject(ctx context.Context, project Project) error UpsertOverride(ctx context.Context, override Override) error + GetOverridesForProject(ctx context.Context, projectKey string) (FlagsState, error) } func ContextWithStore(ctx context.Context, store Store) context.Context { From f8181428c34fbf6b35e99e200ff3242bf6b510a8 Mon Sep 17 00:00:00 2001 From: Mike Zorn Date: Wed, 26 Jun 2024 16:31:26 -0700 Subject: [PATCH 37/79] Overrides make streaming updates (#342) * Fix lint * set up error logging * Create observer * Notify observer when the override is upserted * Streaming for clients works! * rename file to be about stream * Server-side patch streaming * Fix observer deregistration * Scope to project so we don't cross streams * Eliminate extraneous urls --- cmd/dev_server/overrides.go | 4 +- cmd/dev_server/projects.go | 10 +-- internal/dev_server/adapters/middleware.go | 4 +- internal/dev_server/adapters/sdk.go | 21 +++--- internal/dev_server/db/sqlite.go | 20 +++-- internal/dev_server/dev_server.go | 17 ++++- internal/dev_server/model/observer.go | 46 ++++++++++++ .../dev_server/model/observer_middleware.go | 25 +++++++ internal/dev_server/model/observer_test.go | 37 ++++++++++ internal/dev_server/model/override.go | 22 +++++- internal/dev_server/model/store.go | 2 +- internal/dev_server/sdk/get_server_flags.go | 32 -------- .../dev_server/sdk/stream_client_flags.go | 41 +++++++++++ .../dev_server/sdk/stream_server_flags.go | 73 +++++++++++++++++++ internal/dev_server/sdk/streaming.go | 8 +- 15 files changed, 297 insertions(+), 65 deletions(-) create mode 100644 internal/dev_server/model/observer.go create mode 100644 internal/dev_server/model/observer_middleware.go create mode 100644 internal/dev_server/model/observer_test.go delete mode 100644 internal/dev_server/sdk/get_server_flags.go create mode 100644 internal/dev_server/sdk/stream_server_flags.go diff --git a/cmd/dev_server/overrides.go b/cmd/dev_server/overrides.go index 60e734a4..f834440c 100644 --- a/cmd/dev_server/overrides.go +++ b/cmd/dev_server/overrides.go @@ -67,7 +67,7 @@ func addOverride(client dev_server.LocalClient) func(*cobra.Command, []string) e return output.NewCmdOutputError(err, viper.GetString(cliflags.OutputFlag)) } - fmt.Fprintf(cmd.OutOrStdout(), string(res)) + fmt.Fprint(cmd.OutOrStdout(), string(res)) return nil } @@ -110,7 +110,7 @@ func removeOverride(client dev_server.LocalClient) func(*cobra.Command, []string return output.NewCmdOutputError(err, viper.GetString(cliflags.OutputFlag)) } - fmt.Fprintf(cmd.OutOrStdout(), string(res)) + fmt.Fprint(cmd.OutOrStdout(), string(res)) return nil } diff --git a/cmd/dev_server/projects.go b/cmd/dev_server/projects.go index a5444cbc..20879586 100644 --- a/cmd/dev_server/projects.go +++ b/cmd/dev_server/projects.go @@ -44,7 +44,7 @@ func listProjects(client dev_server.LocalClient) func(*cobra.Command, []string) return output.NewCmdOutputError(err, viper.GetString(cliflags.OutputFlag)) } - fmt.Fprintf(cmd.OutOrStdout(), string(res)) + fmt.Fprint(cmd.OutOrStdout(), string(res)) return nil } @@ -82,7 +82,7 @@ func getProject(client dev_server.LocalClient) func(*cobra.Command, []string) er if err != nil { return output.NewCmdOutputError(err, viper.GetString(cliflags.OutputFlag)) } - fmt.Fprintf(cmd.OutOrStdout(), string(res)) + fmt.Fprint(cmd.OutOrStdout(), string(res)) return nil } @@ -120,7 +120,7 @@ func syncProject(client dev_server.LocalClient) func(*cobra.Command, []string) e if err != nil { return output.NewCmdOutputError(err, viper.GetString(cliflags.OutputFlag)) } - fmt.Fprintf(cmd.OutOrStdout(), string(res)) + fmt.Fprint(cmd.OutOrStdout(), string(res)) return nil } @@ -159,7 +159,7 @@ func deleteProject(client dev_server.LocalClient) func(*cobra.Command, []string) return output.NewCmdOutputError(err, viper.GetString(cliflags.OutputFlag)) } - fmt.Fprintf(cmd.OutOrStdout(), string(res)) + fmt.Fprint(cmd.OutOrStdout(), string(res)) return nil } @@ -233,7 +233,7 @@ func addProject(client dev_server.LocalClient) func(*cobra.Command, []string) er return output.NewCmdOutputError(err, viper.GetString(cliflags.OutputFlag)) } - fmt.Fprintf(cmd.OutOrStdout(), string(res)) + fmt.Fprint(cmd.OutOrStdout(), string(res)) return nil } diff --git a/internal/dev_server/adapters/middleware.go b/internal/dev_server/adapters/middleware.go index 3c1282a1..3f2d0a07 100644 --- a/internal/dev_server/adapters/middleware.go +++ b/internal/dev_server/adapters/middleware.go @@ -9,11 +9,11 @@ import ( type ctxKey string // Middleware puts adapters on to the context for consumption by other things -func Middleware(client ldapi.APIClient, eventsUrl, pollingUrl, streamingUrl string) func(handler http.Handler) http.Handler { +func Middleware(client ldapi.APIClient, streamingUrl string) func(handler http.Handler) http.Handler { return func(handler http.Handler) http.Handler { return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { ctx := request.Context() - ctx = WithSdk(ctx, newSdk(eventsUrl, pollingUrl, streamingUrl)) + ctx = WithSdk(ctx, newSdk(streamingUrl)) ctx = WithApi(ctx, NewApi(client)) request = request.WithContext(ctx) handler.ServeHTTP(writer, request) diff --git a/internal/dev_server/adapters/sdk.go b/internal/dev_server/adapters/sdk.go index 6960350a..07a6f7d3 100644 --- a/internal/dev_server/adapters/sdk.go +++ b/internal/dev_server/adapters/sdk.go @@ -2,11 +2,13 @@ package adapters import ( "context" + "log" "time" "github.com/launchdarkly/go-sdk-common/v3/ldcontext" ldsdk "github.com/launchdarkly/go-server-sdk/v7" "github.com/launchdarkly/go-server-sdk/v7/interfaces/flagstate" + "github.com/launchdarkly/go-server-sdk/v7/ldcomponents" ) const ctxKeySdk = ctxKey("adapters.sdk") @@ -20,26 +22,19 @@ func GetSdk(ctx context.Context) Sdk { } type Sdk struct { - eventsUrl string - pollingUrl string streamingUrl string } -func newSdk(eventsUrl, pollingUrl, streamingUrl string) Sdk { +func newSdk(streamingUrl string) Sdk { return Sdk{ - eventsUrl: eventsUrl, - pollingUrl: pollingUrl, streamingUrl: streamingUrl, } } func (s Sdk) GetAllFlagsState(ctx context.Context, ldContext ldcontext.Context, sdkKey string) (flagstate.AllFlags, error) { - config := ldsdk.Config{} - if s.pollingUrl != "" { - config.ServiceEndpoints.Polling = s.pollingUrl - } - if s.eventsUrl != "" { - config.ServiceEndpoints.Events = s.eventsUrl + config := ldsdk.Config{ + DiagnosticOptOut: true, + Events: ldcomponents.NoEvents(), } if s.streamingUrl != "" { config.ServiceEndpoints.Streaming = s.streamingUrl @@ -48,6 +43,10 @@ func (s Sdk) GetAllFlagsState(ctx context.Context, ldContext ldcontext.Context, if err != nil { return flagstate.AllFlags{}, err } + defer func() { + err := ldClient.Close() + log.Printf("error while closing SDK client: %+v", err) + }() flags := ldClient.AllFlagsState(ldContext) return flags, nil } diff --git a/internal/dev_server/db/sqlite.go b/internal/dev_server/db/sqlite.go index a3cdcda4..12e0b4b7 100644 --- a/internal/dev_server/db/sqlite.go +++ b/internal/dev_server/db/sqlite.go @@ -144,27 +144,33 @@ func (s Sqlite) GetOverridesForProject(ctx context.Context, projectKey string) ( return overrides, nil } -func (s Sqlite) UpsertOverride(ctx context.Context, override model.Override) error { +func (s Sqlite) UpsertOverride(ctx context.Context, override model.Override) (model.Override, error) { valueJson, err := override.Value.MarshalJSON() if err != nil { - return errors.Wrap(err, "unable to marshal override value when writing override") + return model.Override{}, errors.Wrap(err, "unable to marshal override value when writing override") } - - _, err = s.database.Exec(` + row := s.database.QueryRowContext(ctx, ` INSERT INTO overrides (project_key, flag_key, value, active) VALUES (?, ?, ?, ?) ON CONFLICT(flag_key, project_key) DO UPDATE SET value=excluded.value, active=excluded.active, - version=version+1; + version=version+1 + RETURNING project_key, flag_key, active, value, version; `, override.ProjectKey, override.FlagKey, valueJson, override.Active, ) - - return err + var tempValue []byte + if err := row.Scan(&override.ProjectKey, &override.FlagKey, &override.Active, &tempValue, &override.Version); err != nil { + return model.Override{}, errors.Wrap(err, "unable to upsert override") + } + if err := json.Unmarshal(tempValue, &override.Value); err != nil { + return model.Override{}, errors.Wrap(err, "unable to unmarshal override value") + } + return override, nil } func (s Sqlite) DeleteOverride(ctx context.Context, projectKey, flagKey string) error { diff --git a/internal/dev_server/dev_server.go b/internal/dev_server/dev_server.go index f1f42669..64c603c6 100644 --- a/internal/dev_server/dev_server.go +++ b/internal/dev_server/dev_server.go @@ -39,10 +39,14 @@ func (c LDClient) RunServer(ctx context.Context, accessToken, baseURI string) { log.Fatal(err) } ss := api.NewStrictServer() - apiServer := api.NewStrictHandler(ss, nil) + apiServer := api.NewStrictHandlerWithOptions(ss, nil, api.StrictHTTPServerOptions{ + RequestErrorHandlerFunc: RequestErrorHandler, + ResponseErrorHandlerFunc: ResponseErrorHandler, + }) r := mux.NewRouter() - r.Use(adapters.Middleware(*ldClient, "https://events.ld.catamorphic.com", "https://relay-stg.ld.catamorphic.com", "https://relay-stg.ld.catamorphic.com")) // TODO add to config + r.Use(adapters.Middleware(*ldClient, "https://relay-stg.ld.catamorphic.com")) // TODO add to config r.Use(model.StoreMiddleware(sqlStore)) + r.Use(model.ObserversMiddleware(model.NewObservers())) sdk.BindRoutes(r) handler := api.HandlerFromMux(apiServer, r) handler = handlers.CombinedLoggingHandler(os.Stdout, handler) @@ -54,3 +58,12 @@ func (c LDClient) RunServer(ctx context.Context, accessToken, baseURI string) { } log.Fatal(server.ListenAndServe()) } + +func ResponseErrorHandler(w http.ResponseWriter, r *http.Request, err error) { + log.Printf("Error while serving response: %+v", err) + http.Error(w, err.Error(), http.StatusInternalServerError) +} +func RequestErrorHandler(w http.ResponseWriter, r *http.Request, err error) { + log.Printf("Error while serving request: %+v", err) + http.Error(w, err.Error(), http.StatusBadRequest) +} diff --git a/internal/dev_server/model/observer.go b/internal/dev_server/model/observer.go new file mode 100644 index 00000000..1728c10c --- /dev/null +++ b/internal/dev_server/model/observer.go @@ -0,0 +1,46 @@ +package model + +import ( + "log" + + "github.com/google/uuid" +) + +type Observer interface { + Handle(interface{}) +} + +type Observers struct { + observers map[uuid.UUID]Observer +} + +func NewObservers() *Observers { + observers := new(Observers) + observers.observers = make(map[uuid.UUID]Observer) + return observers +} + +func (o *Observers) DeregisterObserver(observerId uuid.UUID) bool { + log.Printf("DeregisterObserver: observer %+v", observerId) + for key := range o.observers { + if key == observerId { + delete(o.observers, key) + return true + } + } + return false +} + +func (o *Observers) RegisterObserver(observer Observer) uuid.UUID { + log.Printf("RegisterObserver: observer %+v", observer) + id := uuid.New() + o.observers[id] = observer + return id +} + +func (o *Observers) Notify(event interface{}) { + log.Printf("Notify: event %+v to %d observers", event, len(o.observers)) + for _, observer := range o.observers { + observer.Handle(event) + } +} diff --git a/internal/dev_server/model/observer_middleware.go b/internal/dev_server/model/observer_middleware.go new file mode 100644 index 00000000..ffa3d337 --- /dev/null +++ b/internal/dev_server/model/observer_middleware.go @@ -0,0 +1,25 @@ +package model + +import ( + "context" + "net/http" +) + +const observersKey = ctxKey("model.observers") + +func SetObserversOnContext(ctx context.Context, observers *Observers) context.Context { + return context.WithValue(ctx, observersKey, observers) +} +func GetObserversFromContext(ctx context.Context) *Observers { + return ctx.Value(observersKey).(*Observers) +} +func ObserversMiddleware(observers *Observers) func(handler http.Handler) http.Handler { + return func(handler http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + ctx = SetObserversOnContext(ctx, observers) + r = r.WithContext(ctx) + handler.ServeHTTP(w, r) + }) + } +} diff --git a/internal/dev_server/model/observer_test.go b/internal/dev_server/model/observer_test.go new file mode 100644 index 00000000..c879c620 --- /dev/null +++ b/internal/dev_server/model/observer_test.go @@ -0,0 +1,37 @@ +package model + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +type testObserver struct { + handle func(interface{}) +} + +func (o testObserver) Handle(event interface{}) { o.handle(event) } + +func TestObservers(t *testing.T) { + t.Run("Register then notify yields notification", func(t *testing.T) { + observers := NewObservers() + observerCalled := false + observer := testObserver{handle: func(i interface{}) { + observerCalled = true + }} + observers.RegisterObserver(observer) + observers.Notify("lol") + assert.True(t, observerCalled, "observer should be called") + }) + t.Run("Register, deregister then notify yields NO notification", func(t *testing.T) { + observers := NewObservers() + observer := testObserver{handle: func(i interface{}) { + assert.Fail(t, "should not be called") + }} + id := observers.RegisterObserver(observer) + ok := observers.DeregisterObserver(id) + assert.True(t, ok, "observer should be deregistered") + observers.Notify("lol") + }) + +} diff --git a/internal/dev_server/model/override.go b/internal/dev_server/model/override.go index 90919fbd..6f4241bf 100644 --- a/internal/dev_server/model/override.go +++ b/internal/dev_server/model/override.go @@ -3,7 +3,9 @@ package model import ( "context" "encoding/json" + "github.com/launchdarkly/go-sdk-common/v3/ldvalue" + "github.com/pkg/errors" ) type Override struct { @@ -14,6 +16,12 @@ type Override struct { Version int } +type UpsertOverrideEvent struct { + FlagKey string + ProjectKey string + FlagState FlagState +} + func UpsertOverride(ctx context.Context, projectKey, flagKey, value string) (Override, error) { // TODO: validate flag exists within project, + if the flag type matches @@ -31,11 +39,23 @@ func UpsertOverride(ctx context.Context, projectKey, flagKey, value string) (Ove Version: 1, } store := StoreFromContext(ctx) - err = store.UpsertOverride(ctx, override) + override, err = store.UpsertOverride(ctx, override) if err != nil { return Override{}, err } + observers := GetObserversFromContext(ctx) + project, err := store.GetDevProject(ctx, projectKey) + if err != nil { + return Override{}, errors.Wrap(err, "unable to get project") + } + flagState := override.Apply(project.FlagState[flagKey]) + observers.Notify(UpsertOverrideEvent{ + FlagKey: flagKey, + ProjectKey: projectKey, + FlagState: flagState, + }) + return override, err } diff --git a/internal/dev_server/model/store.go b/internal/dev_server/model/store.go index db706daf..f4ab5284 100644 --- a/internal/dev_server/model/store.go +++ b/internal/dev_server/model/store.go @@ -17,7 +17,7 @@ type Store interface { GetDevProject(ctx context.Context, projectKey string) (*Project, error) DeleteDevProject(ctx context.Context, projectKey string) (bool, error) InsertProject(ctx context.Context, project Project) error - UpsertOverride(ctx context.Context, override Override) error + UpsertOverride(ctx context.Context, override Override) (Override, error) GetOverridesForProject(ctx context.Context, projectKey string) (FlagsState, error) } diff --git a/internal/dev_server/sdk/get_server_flags.go b/internal/dev_server/sdk/get_server_flags.go deleted file mode 100644 index e71e8c4c..00000000 --- a/internal/dev_server/sdk/get_server_flags.go +++ /dev/null @@ -1,32 +0,0 @@ -package sdk - -import ( - "encoding/json" - "net/http" - - "github.com/launchdarkly/ldcli/internal/dev_server/model" - "github.com/pkg/errors" -) - -func StreamServerAllPayload(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - store := model.StoreFromContext(ctx) - projectKey := GetProjectKeyFromContext(ctx) - project, err := store.GetDevProject(ctx, projectKey) - if err != nil { - panic(errors.Wrap(err, "unable to get dev project")) - } - allFlags := project.GetFlagStateWithOverridesForProject(ctx, nil) // TODO fetch overrides - serverFlags := ServerAllPayloadFromFlagsState(allFlags) - jsonBody, err := json.Marshal(serverFlags) - if err != nil { - panic(errors.Wrap(err, "failed to marshal flag state")) - } - updateChan, doneChan := OpenStream(w, r.Context().Done(), Message{"put", jsonBody}) // TODO Wireup updateChan - defer close(updateChan) - err = <-doneChan - if err != nil { - panic(errors.Wrap(err, "stream failure")) - } - -} diff --git a/internal/dev_server/sdk/stream_client_flags.go b/internal/dev_server/sdk/stream_client_flags.go index 7f0f82bd..74bf7407 100644 --- a/internal/dev_server/sdk/stream_client_flags.go +++ b/internal/dev_server/sdk/stream_client_flags.go @@ -2,8 +2,10 @@ package sdk import ( "encoding/json" + "log" "net/http" + "github.com/launchdarkly/go-sdk-common/v3/ldvalue" "github.com/launchdarkly/ldcli/internal/dev_server/model" "github.com/pkg/errors" ) @@ -23,8 +25,47 @@ func StreamClientFlags(w http.ResponseWriter, r *http.Request) { } updateChan, doneChan := OpenStream(w, r.Context().Done(), Message{"put", jsonBody}) // TODO Wireup updateChan defer close(updateChan) + observer := clientFlagsObserver{updateChan, projectKey} + observers := model.GetObserversFromContext(ctx) + observerId := observers.RegisterObserver(observer) + defer func() { + ok := observers.DeregisterObserver(observerId) + if !ok { + log.Printf("unable to remove observer") + } + }() err = <-doneChan if err != nil { panic(errors.Wrap(err, "stream failure")) } } + +type clientFlagsObserver struct { + updateChan chan<- Message + projectKey string +} + +func (c clientFlagsObserver) Handle(event interface{}) { + log.Printf("clientFlagsObserver: handling flag state event: %v", event) + switch event := event.(type) { + case model.UpsertOverrideEvent: + data, err := json.Marshal(clientSidePatchData{ + Key: event.FlagKey, + Version: event.FlagState.Version, + Value: event.FlagState.Value, + }) + if err != nil { + panic(errors.Wrap(err, "failed to marshal flag state in observer")) + } + c.updateChan <- Message{ + Event: "patch", + Data: data, + } + } +} + +type clientSidePatchData struct { + Key string `json:"key"` + Version int `json:"version"` + Value ldvalue.Value `json:"value"` +} diff --git a/internal/dev_server/sdk/stream_server_flags.go b/internal/dev_server/sdk/stream_server_flags.go new file mode 100644 index 00000000..bf23ebfe --- /dev/null +++ b/internal/dev_server/sdk/stream_server_flags.go @@ -0,0 +1,73 @@ +package sdk + +import ( + "encoding/json" + "fmt" + "log" + "net/http" + + "github.com/launchdarkly/ldcli/internal/dev_server/model" + "github.com/pkg/errors" +) + +func StreamServerAllPayload(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + store := model.StoreFromContext(ctx) + projectKey := GetProjectKeyFromContext(ctx) + project, err := store.GetDevProject(ctx, projectKey) + if err != nil { + panic(errors.Wrap(err, "unable to get dev project")) + } + allFlags := project.GetFlagStateWithOverridesForProject(ctx, nil) // TODO fetch overrides + serverFlags := ServerAllPayloadFromFlagsState(allFlags) + jsonBody, err := json.Marshal(serverFlags) + if err != nil { + panic(errors.Wrap(err, "failed to marshal flag state")) + } + updateChan, doneChan := OpenStream(w, r.Context().Done(), Message{"put", jsonBody}) + defer close(updateChan) + observer := serverFlagsObserver{updateChan, projectKey} + observers := model.GetObserversFromContext(ctx) + observerId := observers.RegisterObserver(observer) + defer func() { + ok := observers.DeregisterObserver(observerId) + if !ok { + log.Printf("unable to remove observer") + } + }() + err = <-doneChan + if err != nil { + panic(errors.Wrap(err, "stream failure")) + } +} + +type serverFlagsObserver struct { + updateChan chan<- Message + projectKey string +} + +func (c serverFlagsObserver) Handle(event interface{}) { + log.Printf("clientFlagsObserver: handling flag state event: %v", event) + switch event := event.(type) { + case model.UpsertOverrideEvent: + if event.ProjectKey != c.projectKey { + return + } + data, err := json.Marshal(serverSidePatchData{ + Path: fmt.Sprintf("/flags/%s", event.FlagKey), + Data: ServerFlagFromFlagState(event.FlagKey, event.FlagState), + }) + if err != nil { + panic(errors.Wrap(err, "failed to marshal flag state in observer")) + } + c.updateChan <- Message{ + Event: "patch", + Data: data, + } + } +} + +type serverSidePatchData struct { + Path string `json:"path"` + Data ServerFlag `json:"data"` +} diff --git a/internal/dev_server/sdk/streaming.go b/internal/dev_server/sdk/streaming.go index d5b43a9b..90dba3e5 100644 --- a/internal/dev_server/sdk/streaming.go +++ b/internal/dev_server/sdk/streaming.go @@ -52,8 +52,12 @@ func OpenStream(w http.ResponseWriter, done <-chan struct{}, initialMessage Mess return errors.Wrap(err, "unable to write response") } flusher.Flush() - case <-updateChan: - // TODO do stuff with updateChan + case msg := <-updateChan: + _, err = w.Write(msg.ToPayload()) + if err != nil { + return errors.Wrap(err, "unable to write response") + } + flusher.Flush() case <-done: break loop } From 44f410c1c980a38780097b6b283c974532405f38 Mon Sep 17 00:00:00 2001 From: Mike Zorn Date: Wed, 26 Jun 2024 17:54:49 -0700 Subject: [PATCH 38/79] fix: Overrides weren't being loaded when SDKs were initialized (#345) * Return all the overrides from the db and filter in code * Fetch & apply overrides --- internal/dev_server/api/server.go | 11 +++++++- internal/dev_server/db/sqlite.go | 25 ++++++++++--------- internal/dev_server/model/project.go | 10 ++++++-- internal/dev_server/model/store.go | 2 +- internal/dev_server/sdk/get_client_flags.go | 5 +++- .../dev_server/sdk/stream_client_flags.go | 5 +++- .../dev_server/sdk/stream_server_flags.go | 5 +++- 7 files changed, 44 insertions(+), 19 deletions(-) diff --git a/internal/dev_server/api/server.go b/internal/dev_server/api/server.go index 5f43e17d..b3dae771 100644 --- a/internal/dev_server/api/server.go +++ b/internal/dev_server/api/server.go @@ -63,7 +63,16 @@ func (s Server) GetDevProjectsProjectKey(ctx context.Context, request GetDevProj if err != nil { return nil, err } - response.Overrides = &overrides + respOverrides := make(model.FlagsState) + for _, override := range overrides { + if !override.Active { + continue + } + respOverrides[override.FlagKey] = model.FlagState{ + Value: override.Value, + Version: override.Version, + } + } } } diff --git a/internal/dev_server/db/sqlite.go b/internal/dev_server/db/sqlite.go index 12e0b4b7..0962dc5e 100644 --- a/internal/dev_server/db/sqlite.go +++ b/internal/dev_server/db/sqlite.go @@ -100,11 +100,11 @@ VALUES (?, ?, ?, ?, ?) return err } -func (s Sqlite) GetOverridesForProject(ctx context.Context, projectKey string) (model.FlagsState, error) { +func (s Sqlite) GetOverridesForProject(ctx context.Context, projectKey string) (model.Overrides, error) { rows, err := s.database.QueryContext(ctx, ` - SELECT flag_key, value, version + SELECT flag_key, active, value, version FROM overrides - WHERE project_key = ? AND active=1 + WHERE project_key = ? `, projectKey) if err != nil { @@ -112,14 +112,14 @@ func (s Sqlite) GetOverridesForProject(ctx context.Context, projectKey string) ( } defer rows.Close() - overrides := make(model.FlagsState) + overrides := make(model.Overrides, 0) for rows.Next() { var flagKey string + var active bool var value string var version int - var flagState model.FlagState - err = rows.Scan(&flagKey, &value, &version) + err = rows.Scan(&flagKey, &active, &value, &version) if err != nil { return nil, err } @@ -129,12 +129,13 @@ func (s Sqlite) GetOverridesForProject(ctx context.Context, projectKey string) ( if err != nil { return nil, err } - - flagState = model.FlagState{ - Value: ldValue, - Version: version, - } - overrides[flagKey] = flagState + overrides = append(overrides, model.Override{ + ProjectKey: projectKey, + FlagKey: flagKey, + Value: ldValue, + Active: active, + Version: version, + }) } if err = rows.Err(); err != nil { diff --git a/internal/dev_server/model/project.go b/internal/dev_server/model/project.go index f26c4dff..cb74fb97 100644 --- a/internal/dev_server/model/project.go +++ b/internal/dev_server/model/project.go @@ -6,6 +6,7 @@ import ( "github.com/launchdarkly/go-sdk-common/v3/ldcontext" "github.com/launchdarkly/ldcli/internal/dev_server/adapters" + "github.com/pkg/errors" ) type Project struct { @@ -48,7 +49,12 @@ func CreateProject(ctx context.Context, projectKey, sourceEnvironmentKey string, return project, nil } -func (p Project) GetFlagStateWithOverridesForProject(ctx context.Context, overrides Overrides) FlagsState { +func (p Project) GetFlagStateWithOverridesForProject(ctx context.Context) (FlagsState, error) { + store := StoreFromContext(ctx) + overrides, err := store.GetOverridesForProject(ctx, p.Key) + if err != nil { + return FlagsState{}, errors.Wrapf(err, "unable to fetch overrides for project %s", p.Key) + } withOverrides := make(FlagsState, len(p.FlagState)) for flagKey, flagState := range p.FlagState { if override, ok := overrides.GetFlag(flagKey); ok { @@ -56,5 +62,5 @@ func (p Project) GetFlagStateWithOverridesForProject(ctx context.Context, overri } withOverrides[flagKey] = flagState } - return withOverrides + return withOverrides, nil } diff --git a/internal/dev_server/model/store.go b/internal/dev_server/model/store.go index f4ab5284..0d9f4308 100644 --- a/internal/dev_server/model/store.go +++ b/internal/dev_server/model/store.go @@ -18,7 +18,7 @@ type Store interface { DeleteDevProject(ctx context.Context, projectKey string) (bool, error) InsertProject(ctx context.Context, project Project) error UpsertOverride(ctx context.Context, override Override) (Override, error) - GetOverridesForProject(ctx context.Context, projectKey string) (FlagsState, error) + GetOverridesForProject(ctx context.Context, projectKey string) (Overrides, error) } func ContextWithStore(ctx context.Context, store Store) context.Context { diff --git a/internal/dev_server/sdk/get_client_flags.go b/internal/dev_server/sdk/get_client_flags.go index d100e23d..8bb8ecbf 100644 --- a/internal/dev_server/sdk/get_client_flags.go +++ b/internal/dev_server/sdk/get_client_flags.go @@ -16,7 +16,10 @@ func GetClientFlags(w http.ResponseWriter, r *http.Request) { if err != nil { panic(errors.Wrap(err, "unable to get dev project")) } - allFlags := project.GetFlagStateWithOverridesForProject(ctx, nil) // TODO fetch overrides + allFlags, err := project.GetFlagStateWithOverridesForProject(ctx) + if err != nil { + panic(errors.Wrap(err, "failed to get flag state")) + } jsonBody, err := json.Marshal(allFlags) if err != nil { panic(errors.Wrap(err, "failed to marshal flag state")) diff --git a/internal/dev_server/sdk/stream_client_flags.go b/internal/dev_server/sdk/stream_client_flags.go index 74bf7407..62ef28fd 100644 --- a/internal/dev_server/sdk/stream_client_flags.go +++ b/internal/dev_server/sdk/stream_client_flags.go @@ -18,7 +18,10 @@ func StreamClientFlags(w http.ResponseWriter, r *http.Request) { if err != nil { panic(errors.Wrap(err, "unable to get dev project")) } - allFlags := project.GetFlagStateWithOverridesForProject(ctx, nil) // TODO fetch overrides + allFlags, err := project.GetFlagStateWithOverridesForProject(ctx) + if err != nil { + panic(errors.Wrap(err, "failed to get flag state")) + } jsonBody, err := json.Marshal(allFlags) if err != nil { panic(errors.Wrap(err, "failed to marshal flag state")) diff --git a/internal/dev_server/sdk/stream_server_flags.go b/internal/dev_server/sdk/stream_server_flags.go index bf23ebfe..4ef2b1d9 100644 --- a/internal/dev_server/sdk/stream_server_flags.go +++ b/internal/dev_server/sdk/stream_server_flags.go @@ -18,7 +18,10 @@ func StreamServerAllPayload(w http.ResponseWriter, r *http.Request) { if err != nil { panic(errors.Wrap(err, "unable to get dev project")) } - allFlags := project.GetFlagStateWithOverridesForProject(ctx, nil) // TODO fetch overrides + allFlags, err := project.GetFlagStateWithOverridesForProject(ctx) + if err != nil { + panic(errors.Wrap(err, "failed to get flag state")) + } serverFlags := ServerAllPayloadFromFlagsState(allFlags) jsonBody, err := json.Marshal(serverFlags) if err != nil { From cf687caef02d0e20a9a1ed683f682b6128241e39 Mon Sep 17 00:00:00 2001 From: Hosh Date: Thu, 27 Jun 2024 09:55:11 +0100 Subject: [PATCH 39/79] [sc-248413] moonshots deploy ld dev server into the dev (#338) --- .goreleaser.yaml | 7 +++++-- Dockerfile.goreleaser | 3 --- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.goreleaser.yaml b/.goreleaser.yaml index e2daddf2..2bc2299c 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -9,8 +9,11 @@ builds: - 386 - amd64 - arm64 - env: - - CGO_ENABLED=0 + ldflags: + - -s + - -w + - -linkmode external + - -extldflags -static dockers: # AMD64 - image_templates: diff --git a/Dockerfile.goreleaser b/Dockerfile.goreleaser index 6f220799..7f83c35d 100644 --- a/Dockerfile.goreleaser +++ b/Dockerfile.goreleaser @@ -1,8 +1,5 @@ FROM alpine:3.19.1 -RUN apk update -RUN apk add --no-cache git - COPY ldcli /ldcli LABEL homepage="https://www.launchdarkly.com" From 314de9fbda8a4e49aa63f073ef318d225d5aa5c6 Mon Sep 17 00:00:00 2001 From: Keegan Watkins <109103953+kwatkins-ld@users.noreply.github.com> Date: Thu, 27 Jun 2024 10:53:46 -0500 Subject: [PATCH 40/79] feat: Add support for flag overrides from the devserver UI (#343) --- internal/dev_server/ui/.prettierrc | 6 + internal/dev_server/ui/README.md | 6 +- internal/dev_server/ui/src/App.css | 33 ++- internal/dev_server/ui/src/App.tsx | 267 +++++++++++++++++--- internal/dev_server/ui/src/IconProvider.tsx | 16 +- internal/dev_server/ui/src/main.tsx | 10 +- internal/dev_server/ui/vite.config.ts | 12 +- 7 files changed, 280 insertions(+), 70 deletions(-) create mode 100644 internal/dev_server/ui/.prettierrc diff --git a/internal/dev_server/ui/.prettierrc b/internal/dev_server/ui/.prettierrc new file mode 100644 index 00000000..dc75c8a8 --- /dev/null +++ b/internal/dev_server/ui/.prettierrc @@ -0,0 +1,6 @@ +{ + "trailingComma": "all", + "tabWidth": 2, + "semi": true, + "singleQuote": true +} diff --git a/internal/dev_server/ui/README.md b/internal/dev_server/ui/README.md index bb156850..9d0b4bcd 100644 --- a/internal/dev_server/ui/README.md +++ b/internal/dev_server/ui/README.md @@ -17,9 +17,9 @@ If you are developing a production application, we recommend updating the config export default { // other rules... parserOptions: { - ecmaVersion: "latest", - sourceType: "module", - project: ["./tsconfig.json", "./tsconfig.node.json"], + ecmaVersion: 'latest', + sourceType: 'module', + project: ['./tsconfig.json', './tsconfig.node.json'], tsconfigRootDir: __dirname, }, }; diff --git a/internal/dev_server/ui/src/App.css b/internal/dev_server/ui/src/App.css index 0d0dd6ec..7bd331de 100644 --- a/internal/dev_server/ui/src/App.css +++ b/internal/dev_server/ui/src/App.css @@ -1,6 +1,6 @@ -@import url("../node_modules/@launchpad-ui/tokens/dist/index.css"); -@import url("../node_modules/@launchpad-ui/tokens/dist/media-queries.css"); -@import url("../node_modules/@launchpad-ui/tokens/dist/themes.css"); +@import url('../node_modules/@launchpad-ui/tokens/dist/index.css'); +@import url('../node_modules/@launchpad-ui/tokens/dist/media-queries.css'); +@import url('../node_modules/@launchpad-ui/tokens/dist/themes.css'); html, body, @@ -14,11 +14,20 @@ body, padding: 2rem; } +.container { + max-width: 40rem; + margin: 0 auto; +} + +.only-show-overrides-label { + display: flex; + flex-direction: row; + align-items: center; +} + ul.flags-list { margin: 0; padding: 0; - max-width: 40rem; - margin: 0 auto; } ul.flags-list li { @@ -30,11 +39,23 @@ ul.flags-list li { align-items: center; } +ul.flags-list li .flag-value { + overflow-x: overlay; + white-space: nowrap; + display: flex; + flex-direction: row; + align-items: center; + flex-shrink: 1; +} + code { background-color: var(--lp-color-bg-ui-secondary); border: 1px solid var(--lp-color-border-ui-primary); border-radius: 2px; - color: var(--lp-color-pink-600); font-size: 87.5%; padding: 0.125rem 0.25rem; } + +code.has-override { + color: var(--lp-color-pink-600); +} diff --git a/internal/dev_server/ui/src/App.tsx b/internal/dev_server/ui/src/App.tsx index 12110669..601e3efb 100644 --- a/internal/dev_server/ui/src/App.tsx +++ b/internal/dev_server/ui/src/App.tsx @@ -1,88 +1,271 @@ -import { LDFlagSet, LDFlagValue } from "launchdarkly-js-client-sdk"; -import { Switch } from "@launchpad-ui/components"; -import { CopyToClipboard, InlineEdit, TextField } from "@launchpad-ui/core"; -import "./App.css"; -import { useEffect, useState } from "react"; +import { LDFlagSet, LDFlagValue } from 'launchdarkly-js-client-sdk'; +import { + Button, + Checkbox, + IconButton, + Label, + Switch, +} from '@launchpad-ui/components'; +import { + Box, + CopyToClipboard, + InlineEdit, + TextField, +} from '@launchpad-ui/core'; +import Theme from '@launchpad-ui/tokens'; +import './App.css'; +import { useEffect, useState } from 'react'; +import { Icon } from '@launchpad-ui/icons'; function App() { const [flags, setFlags] = useState(null); + const [overrides, setOverrides] = useState | null>(null); + const [onlyShowOverrides, setOnlyShowOverrides] = useState(false); + const overridesPresent = overrides && Object.keys(overrides).length > 0; - useEffect(() => { - fetch("/api/dev/projects/default?expand=overrides") + const updateOverride = (flagKey: string, overrideValue: LDFlagValue) => { + fetch(`api/dev/projects/default/overrides/${flagKey}`, { + method: 'PUT', + body: JSON.stringify(overrideValue), + }) .then(async (res) => { - if (res.ok) { - const flags = (await res.json()).flagsState; - const sortedFlags = Object.keys(flags) - .sort((a, b) => a.localeCompare(b)) - .reduce>((accum, flagKey) => { - accum[flagKey] = flags[flagKey]; + if (!res.ok) { + return; // todo + } + + const updatedOverrides = { + ...overrides, + ...{ + [flagKey]: { + value: overrideValue, + }, + }, + }; + + setOverrides(updatedOverrides); + }) + .catch((e) => { + // todo + }); + }; - return accum; - }, {}); + const removeOverride = (flagKey: string, updateState: boolean = true) => { + return fetch(`api/dev/projects/default/overrides/${flagKey}`, { + method: 'DELETE', + }) + .then((res) => { + // todo: clean this up. + // + // In the remove-all-override case, we need to fan out and make the + // request for every override, so we don't want to be interleaving + // local state updates. Expect the consumer to update the local state + // when all requests are done + if (res.ok && updateState) { + const updatedOverrides = { ...overrides }; + delete updatedOverrides[flagKey]; - setFlags(sortedFlags); + setOverrides(updatedOverrides); } }) .catch((e) => { - // todo: handle failure to load flag data + // todo + }); + }; + + // Fetch flags / overrides on mount + useEffect(() => { + fetch('/api/dev/projects/default?expand=overrides') + .then(async (res) => { + if (!res.ok) { + return; // todo + } + + const json = await res.json(); + const { flagsState: flags, overrides } = json; + const sortedFlags = Object.keys(flags) + .sort((a, b) => a.localeCompare(b)) + .reduce>((accum, flagKey) => { + accum[flagKey] = flags[flagKey]; + + return accum; + }, {}); + + setFlags(sortedFlags); + setOverrides(overrides); + }) + .catch((e) => { + // todo }); }, []); + if (!flags) { + return null; + } + return ( <> -
+
+ + + +
    - {flags && - Object.entries(flags).map(([flagKey, { value: flagValue }]) => { - let valueNode; + {Object.entries(flags).map(([flagKey, { value: flagValue }]) => { + const overrideValue = overrides?.[flagKey]?.value; + const hasOverride = overrideValue !== undefined; + let valueNode; - if (typeof flagValue === "boolean") { + if (onlyShowOverrides && !hasOverride) { + return null; + } + + switch (typeof flagValue) { + case 'boolean': valueNode = ( { - // todo + updateOverride(flagKey, newValue); }} /> ); - } else if (typeof flagValue === "number") { + break; + case 'number': valueNode = ( { - // todo + updateOverride(flagKey, Number(e.target.value)); }} /> ); - } else { + break; + case 'string': + valueNode = ( + { + updateOverride(flagKey, newValue); + }} + renderInput={} + > + {hasOverride ? overrideValue : flagValue} + + ); + break; + default: valueNode = ( { - // todo + updateOverride(flagKey, JSON.parse(newValue)); }} renderInput={} > - {JSON.stringify(flagValue)} + {JSON.stringify(hasOverride ? overrideValue : flagValue)} ); - } + } - return ( -
  • - - {flagKey} - -
    {valueNode}
    -
  • - ); - })} + return ( +
  • + + + {flagKey} + + +
    {valueNode}
    + + {hasOverride && ( + { + removeOverride(flagKey); + }} + variant="destructive" + /> + )} + +
  • + ); + })}
diff --git a/internal/dev_server/ui/src/IconProvider.tsx b/internal/dev_server/ui/src/IconProvider.tsx index 33133068..88fd01c8 100644 --- a/internal/dev_server/ui/src/IconProvider.tsx +++ b/internal/dev_server/ui/src/IconProvider.tsx @@ -1,7 +1,7 @@ -import type { ReactNode } from "react"; -import { useEffect } from "react"; -import { IconContext } from "@launchpad-ui/icons"; -import sprite from "@launchpad-ui/icons/sprite.svg"; +import type { ReactNode } from 'react'; +import { useEffect } from 'react'; +import { IconContext } from '@launchpad-ui/icons'; +import sprite from '@launchpad-ui/icons/sprite.svg'; export function IconProvider({ children }: { children: ReactNode }) { useEffect(() => { @@ -11,10 +11,10 @@ export function IconProvider({ children }: { children: ReactNode }) { const parser = new DOMParser(); const content = parser.parseFromString( data, - "image/svg+xml", + 'image/svg+xml', ).documentElement; - content.id = "lp-icons-sprite"; - content.style.display = "none"; + content.id = 'lp-icons-sprite'; + content.style.display = 'none'; document.body.appendChild(content); }) .catch((_err) => { @@ -23,6 +23,6 @@ export function IconProvider({ children }: { children: ReactNode }) { }, []); return ( - {children} + {children} ); } diff --git a/internal/dev_server/ui/src/main.tsx b/internal/dev_server/ui/src/main.tsx index 7d268e02..d719fff9 100644 --- a/internal/dev_server/ui/src/main.tsx +++ b/internal/dev_server/ui/src/main.tsx @@ -1,9 +1,9 @@ -import React from "react"; -import ReactDOM from "react-dom/client"; -import App from "./App.tsx"; -import { IconProvider } from "./IconProvider.tsx"; +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App.tsx'; +import { IconProvider } from './IconProvider.tsx'; -ReactDOM.createRoot(document.getElementById("root")!).render( +ReactDOM.createRoot(document.getElementById('root')!).render( diff --git a/internal/dev_server/ui/vite.config.ts b/internal/dev_server/ui/vite.config.ts index 802570f0..94a5936e 100644 --- a/internal/dev_server/ui/vite.config.ts +++ b/internal/dev_server/ui/vite.config.ts @@ -1,16 +1,16 @@ -import { defineConfig } from "vite"; -import react from "@vitejs/plugin-react"; -import svgr from "vite-plugin-svgr"; +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import svgr from 'vite-plugin-svgr'; // https://vitejs.dev/config/ export default defineConfig({ plugins: [react(), svgr()], server: { proxy: { - "/api": { - target: "http://localhost:8765", + '/api': { + target: 'http://localhost:8765', changeOrigin: true, - rewrite: (path: string) => path.replace(/^\/api/, ""), + rewrite: (path: string) => path.replace(/^\/api/, ''), }, }, }, From 7273f8d58e51b57d25692e0f8d452cc4be745554 Mon Sep 17 00:00:00 2001 From: Mike Zorn Date: Thu, 27 Jun 2024 10:05:00 -0700 Subject: [PATCH 41/79] feat: Move devserver db to XDG_STATE_HOME (#344) * Move devserver db to XDG_STATE_HOME * Move devserver db to XDG_STATE_HOME * Use XDG lib instead --- go.mod | 1 + go.sum | 3 + internal/dev_server/dev_server.go | 16 +- vendor/github.com/adrg/xdg/CODE_OF_CONDUCT.md | 77 +++++ vendor/github.com/adrg/xdg/CONTRIBUTING.md | 135 +++++++++ vendor/github.com/adrg/xdg/LICENSE | 21 ++ vendor/github.com/adrg/xdg/README.md | 280 ++++++++++++++++++ vendor/github.com/adrg/xdg/base_dirs.go | 68 +++++ vendor/github.com/adrg/xdg/codecov.yml | 11 + vendor/github.com/adrg/xdg/doc.go | 99 +++++++ .../adrg/xdg/internal/pathutil/pathutil.go | 78 +++++ .../xdg/internal/pathutil/pathutil_plan9.go | 29 ++ .../xdg/internal/pathutil/pathutil_unix.go | 32 ++ .../xdg/internal/pathutil/pathutil_windows.go | 64 ++++ vendor/github.com/adrg/xdg/paths_darwin.go | 60 ++++ vendor/github.com/adrg/xdg/paths_plan9.go | 55 ++++ vendor/github.com/adrg/xdg/paths_unix.go | 71 +++++ vendor/github.com/adrg/xdg/paths_windows.go | 168 +++++++++++ vendor/github.com/adrg/xdg/user_dirs.go | 40 +++ vendor/github.com/adrg/xdg/xdg.go | 218 ++++++++++++++ vendor/modules.txt | 4 + 21 files changed, 1527 insertions(+), 3 deletions(-) create mode 100644 vendor/github.com/adrg/xdg/CODE_OF_CONDUCT.md create mode 100644 vendor/github.com/adrg/xdg/CONTRIBUTING.md create mode 100644 vendor/github.com/adrg/xdg/LICENSE create mode 100644 vendor/github.com/adrg/xdg/README.md create mode 100644 vendor/github.com/adrg/xdg/base_dirs.go create mode 100644 vendor/github.com/adrg/xdg/codecov.yml create mode 100644 vendor/github.com/adrg/xdg/doc.go create mode 100644 vendor/github.com/adrg/xdg/internal/pathutil/pathutil.go create mode 100644 vendor/github.com/adrg/xdg/internal/pathutil/pathutil_plan9.go create mode 100644 vendor/github.com/adrg/xdg/internal/pathutil/pathutil_unix.go create mode 100644 vendor/github.com/adrg/xdg/internal/pathutil/pathutil_windows.go create mode 100644 vendor/github.com/adrg/xdg/paths_darwin.go create mode 100644 vendor/github.com/adrg/xdg/paths_plan9.go create mode 100644 vendor/github.com/adrg/xdg/paths_unix.go create mode 100644 vendor/github.com/adrg/xdg/paths_windows.go create mode 100644 vendor/github.com/adrg/xdg/user_dirs.go create mode 100644 vendor/github.com/adrg/xdg/xdg.go diff --git a/go.mod b/go.mod index 03aa5c2d..3deadd32 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/launchdarkly/ldcli go 1.22 require ( + github.com/adrg/xdg v0.4.0 github.com/charmbracelet/bubbles v0.18.0 github.com/charmbracelet/bubbletea v0.25.0 github.com/charmbracelet/glamour v0.6.0 diff --git a/go.sum b/go.sum index d372232b..7165d731 100644 --- a/go.sum +++ b/go.sum @@ -34,6 +34,8 @@ dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk= +github.com/adrg/xdg v0.4.0 h1:RzRqFcjH4nE5C6oTAxhBtoE2IRyjBSa62SCbyPidvls= +github.com/adrg/xdg v0.4.0/go.mod h1:N6ag73EX4wyxeaoeHctc1mas01KZgsj5tYiAIwqJE/E= github.com/alecthomas/chroma v0.10.0 h1:7XDcGkCQopCNKjZHfYrNLraA+M7e0fMiJ/Mfikbfjek= github.com/alecthomas/chroma v0.10.0/go.mod h1:jtJATyUxlIORhUOFNA9NZDWGAQ8wpxQQqNSB4rjA/1s= github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ= @@ -433,6 +435,7 @@ golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/internal/dev_server/dev_server.go b/internal/dev_server/dev_server.go index 64c603c6..5d189f3c 100644 --- a/internal/dev_server/dev_server.go +++ b/internal/dev_server/dev_server.go @@ -7,15 +7,15 @@ import ( "net/http" "os" + "github.com/adrg/xdg" "github.com/gorilla/handlers" "github.com/gorilla/mux" - "github.com/launchdarkly/ldcli/internal/dev_server/sdk" - "github.com/launchdarkly/ldcli/internal/client" "github.com/launchdarkly/ldcli/internal/dev_server/adapters" "github.com/launchdarkly/ldcli/internal/dev_server/api" "github.com/launchdarkly/ldcli/internal/dev_server/db" "github.com/launchdarkly/ldcli/internal/dev_server/model" + "github.com/launchdarkly/ldcli/internal/dev_server/sdk" ) type Client interface { @@ -34,7 +34,9 @@ func NewClient(cliVersion string) LDClient { func (c LDClient) RunServer(ctx context.Context, accessToken, baseURI string) { ldClient := client.New(accessToken, baseURI, c.cliVersion) - sqlStore, err := db.NewSqlite(ctx, "devserver.db") + dbPath := getDBPath() + log.Printf("Using database at %s", dbPath) + sqlStore, err := db.NewSqlite(ctx, getDBPath()) if err != nil { log.Fatal(err) } @@ -67,3 +69,11 @@ func RequestErrorHandler(w http.ResponseWriter, r *http.Request, err error) { log.Printf("Error while serving request: %+v", err) http.Error(w, err.Error(), http.StatusBadRequest) } + +func getDBPath() string { + dbFilePath, err := xdg.StateFile("ldcli/dev_server.db") + if err != nil { + log.Fatalf("Unable to create state directory: %s", err) + } + return dbFilePath +} diff --git a/vendor/github.com/adrg/xdg/CODE_OF_CONDUCT.md b/vendor/github.com/adrg/xdg/CODE_OF_CONDUCT.md new file mode 100644 index 00000000..75349e53 --- /dev/null +++ b/vendor/github.com/adrg/xdg/CODE_OF_CONDUCT.md @@ -0,0 +1,77 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, +body size, disability, ethnicity, sex characteristics, gender identity and +expression, level of experience, education, socio-economic status, nationality, +personal appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behaviour that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behaviour by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or + advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behaviour and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behaviour. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviour that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behaviour may be +reported by contacting the project team at adrg@epistack.com. All complaints +will be reviewed and investigated and will result in a response that is deemed +necessary and appropriate to the circumstances. The project team is obligated to +maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 1.4, available at +https://www.contributor-covenant.org/version/1/4/code-of-conduct.html + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see +https://www.contributor-covenant.org/faq diff --git a/vendor/github.com/adrg/xdg/CONTRIBUTING.md b/vendor/github.com/adrg/xdg/CONTRIBUTING.md new file mode 100644 index 00000000..006f146b --- /dev/null +++ b/vendor/github.com/adrg/xdg/CONTRIBUTING.md @@ -0,0 +1,135 @@ +# Contributing to this project + +Contributions in the form of pull requests, issues or just general feedback, +are always welcome. Please take a moment to review this document in order to +make the contribution process easy and effective for everyone involved. + +Following these guidelines helps to communicate that you respect the time of +the developers managing and developing this open source project. In return, +they should reciprocate that respect in addressing your issue or assessing +patches and features. + +## Using the issue tracker + +The issue tracker is the preferred channel for [bug reports](#bugs), +[features requests](#features) and [submitting pull +requests](#pull-requests), but please respect the following restrictions: + +* Please **do not** use the issue tracker for personal support requests (use + [Stack Overflow](http://stackoverflow.com) or IRC). +* Please **do not** derail or troll issues. Keep the discussion on topic and + respect the opinions of others. + + +## Bug reports + +A bug is a _demonstrable problem_ that is caused by the code in the repository. +Good bug reports are extremely helpful - thank you! + +Guidelines for bug reports: + +1. **Use the GitHub issue search** — check if the issue has already been + reported. +2. **Check if the issue has been fixed** — try to reproduce it using the + latest `master` or development branch in the repository. +3. **Isolate the problem** — create a reduced test case. + +A good bug report shouldn't leave others needing to chase you up for more +information. Please try to be as detailed as possible in your report. What is +your environment? What steps will reproduce the issue? What browser(s) and OS +experience the problem? What would you expect to be the outcome? All these +details will help people to fix any potential bugs. + +Example: + +> Short and descriptive example bug report title +> +> A summary of the issue and the browser/OS environment in which it occurs. If +> suitable, include the steps required to reproduce the bug. +> +> 1. This is the first step +> 2. This is the second step +> 3. Further steps, etc. +> +> `` - a link to the reduced test case +> +> Any other information you want to share that is relevant to the issue being +> reported. This might include the lines of code that you have identified as +> causing the bug, and potential solutions (and your opinions on their +> merits). + + + +## Feature requests + +Feature requests are welcome. But take a moment to find out whether your idea +fits with the scope and aims of the project. It's up to *you* to make a strong +case to convince the project's developers of the merits of this feature. Please +provide as much detail and context as possible. + + + +## Pull requests + +Good pull requests - patches, improvements, new features - are a fantastic +help. They should remain focused in scope and avoid containing unrelated +commits. + +**Please ask first** before embarking on any significant pull request (e.g. +implementing features, refactoring code, porting to a different language), +otherwise you risk spending a lot of time working on something that the +project's developers might not want to merge into the project. + +Please adhere to the coding conventions used throughout a project (indentation, +accurate comments, etc.) and any other requirements (such as test coverage). + +Follow this process if you'd like your work considered for inclusion in the +project: + +1. [Fork](http://help.github.com/fork-a-repo/) the project, clone your fork, + and configure the remotes: + + ```bash + # Clone your fork of the repo into the current directory + git clone https://github.com// + # Navigate to the newly cloned directory + cd + # Assign the original repo to a remote called "upstream" + git remote add upstream https://github.com// + ``` + +2. If you cloned a while ago, get the latest changes from upstream: + + ```bash + git checkout + git pull upstream + ``` + +3. Create a new topic branch (off the main project development branch) to + contain your feature, change, or fix: + + ```bash + git checkout -b + ``` + +4. Commit your changes in logical chunks and use descriptive commit messages. + Use [interactive rebase](https://help.github.com/articles/interactive-rebase) + to tidy up your commits before making them public. + +5. Locally merge (or rebase) the upstream development branch into your topic branch: + + ```bash + git pull [--rebase] upstream + ``` + +6. Push your topic branch up to your fork: + + ```bash + git push origin + ``` + +7. [Open a Pull Request](https://help.github.com/articles/using-pull-requests/) + with a clear title and description. + +**IMPORTANT**: By submitting a patch, you agree to allow the project owner to +license your work under the same license as that used by the project. diff --git a/vendor/github.com/adrg/xdg/LICENSE b/vendor/github.com/adrg/xdg/LICENSE new file mode 100644 index 00000000..7307e1b7 --- /dev/null +++ b/vendor/github.com/adrg/xdg/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2014 Adrian-George Bostan + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/vendor/github.com/adrg/xdg/README.md b/vendor/github.com/adrg/xdg/README.md new file mode 100644 index 00000000..b55403c2 --- /dev/null +++ b/vendor/github.com/adrg/xdg/README.md @@ -0,0 +1,280 @@ +

+
+ xdg logo +
+

+ +

Go implementation of the XDG Base Directory Specification and XDG user directories.

+ +

+ + Build status + + + Code coverage + + + pkg.go.dev documentation + + + MIT license + +
+ + Go report card + + + Awesome Go + + + GitHub contributors + + + GitHub open issues + + + Buy me a coffee + +

+ +Provides an implementation of the [XDG Base Directory Specification](https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html). +The specification defines a set of standard paths for storing application files, +including data and configuration files. For portability and flexibility reasons, +applications should use the XDG defined locations instead of hardcoding paths. +The package also includes the locations of well known [user directories](https://wiki.archlinux.org/index.php/XDG_user_directories), as well as +other common directories such as fonts and applications. + +The current implementation supports **most flavors of Unix**, **Windows**, **macOS** and **Plan 9**. +On Windows, where XDG environment variables are not usually set, the package uses [Known Folders](https://docs.microsoft.com/en-us/windows/win32/shell/known-folders) +as defaults. Therefore, appropriate locations are used for common [folders](https://docs.microsoft.com/en-us/windows/win32/shell/knownfolderid) which may have been redirected. + +See usage [examples](#usage) below. Full documentation can be found at https://pkg.go.dev/github.com/adrg/xdg. + +## Installation + go get github.com/adrg/xdg + +## Default locations + +The package defines sensible defaults for XDG variables which are empty or not +present in the environment. + +- On Unix-like operating systems, XDG environment variables are tipically defined. +Appropriate default locations are used for the environment variables which are not set. +- On Windows, XDG environment variables are usually not set. If that is the case, +the package relies on the appropriate [Known Folders](https://docs.microsoft.com/en-us/windows/win32/shell/knownfolderid). +Sensible fallback locations are used for the folders which are not set. + +### XDG Base Directory + +
+ Unix-like operating systems +
+ +| |

Unix

|

macOS

|

Plan 9

| +| :------------------------------------------------------------: | :-----------------------------------------------------------------------: | :-------------------------------------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------: | +| XDG_DATA_HOME | ~/.local/share | ~/Library/Application Support | $home/lib | +| XDG_DATA_DIRS | /usr/local/share
/usr/share | /Library/Application Support | /lib | +| XDG_CONFIG_HOME | ~/.config | ~/Library/Application Support | $home/lib | +| XDG_CONFIG_DIRS | /etc/xdg | ~/Library/Preferences
/Library/Application Support
/Library/Preferences | /lib | +| XDG_STATE_HOME | ~/.local/state | ~/Library/Application Support | $home/lib/state | +| XDG_CACHE_HOME | ~/.cache | ~/Library/Caches | $home/lib/cache | +| XDG_RUNTIME_DIR | /run/user/UID | ~/Library/Application Support | /tmp | + +
+ +
+ Microsoft Windows +
+ +| |

Known Folder(s)

|

Fallback(s)

| +| :------------------------------------------------------------: | :---------------------------------------------------------------------------------------: | :------------------------------------------------------------------------------: | +| XDG_DATA_HOME | LocalAppData | %LOCALAPPDATA% | +| XDG_DATA_DIRS | RoamingAppData
ProgramData | %APPADATA%
%ProgramData% | +| XDG_CONFIG_HOME | LocalAppData | %LOCALAPPDATA% | +| XDG_CONFIG_DIRS | ProgramData
RoamingAppData | %ProgramData%
%APPDATA% | +| XDG_STATE_HOME | LocalAppData | %LOCALAPPDATA% | +| XDG_CACHE_HOME | LocalAppData\cache | %LOCALAPPDATA%\cache | +| XDG_RUNTIME_DIR | LocalAppData | %LOCALAPPDATA% | + +
+ +### XDG user directories + +
+ Unix-like operating systems +
+ +| |

Unix

|

macOS

|

Plan 9

| +| :--------------------------------------------------------------: | :-------------------------------------------------------------------------: | :---------------------------------------------------------------------------: | :---------------------------------------------------------------------------: | +| XDG_DESKTOP_DIR | ~/Desktop | ~/Desktop | $home/desktop | +| XDG_DOWNLOAD_DIR | ~/Downloads | ~/Downloads | $home/downloads | +| XDG_DOCUMENTS_DIR | ~/Documents | ~/Documents | $home/documents | +| XDG_MUSIC_DIR | ~/Music | ~/Music | $home/music | +| XDG_PICTURES_DIR | ~/Pictures | ~/Pictures | $home/pictures | +| XDG_VIDEOS_DIR | ~/Videos | ~/Movies | $home/videos | +| XDG_TEMPLATES_DIR | ~/Templates | ~/Templates | $home/templates | +| XDG_PUBLICSHARE_DIR | ~/Public | ~/Public | $home/public | + +
+ +
+ Microsoft Windows +
+ +| |

Known Folder(s)

|

Fallback(s)

| +| :--------------------------------------------------------------: | :-----------------------------------------------------------------------------------------: | :--------------------------------------------------------------------------------: | +| XDG_DESKTOP_DIR | Desktop | %USERPROFILE%\Desktop | +| XDG_DOWNLOAD_DIR | Downloads | %USERPROFILE%\Downloads | +| XDG_DOCUMENTS_DIR | Documents | %USERPROFILE%\Documents | +| XDG_MUSIC_DIR | Music | %USERPROFILE%\Music | +| XDG_PICTURES_DIR | Pictures | %USERPROFILE%\Pictures | +| XDG_VIDEOS_DIR | Videos | %USERPROFILE%\Videos | +| XDG_TEMPLATES_DIR | Templates | %APPDATA%\Microsoft\Windows\Templates | +| XDG_PUBLICSHARE_DIR | Public | %PUBLIC% | + +
+ +### Other directories + +
+ Unix-like operating systems +
+ +| |

Unix

|

macOS

|

Plan 9

| +| :-----------------------------------------------------------: | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------------------------------: | :------------------------------------------------------------------------: | +| Home | $HOME | $HOME | $home | +| Applications | $XDG_DATA_HOME/applications
~/.local/share/applications
/usr/local/share/applications
/usr/share/applications
$XDG_DATA_DIRS/applications | /Applications | $home/bin
/bin | +| Fonts | $XDG_DATA_HOME/fonts
~/.fonts
~/.local/share/fonts
/usr/local/share/fonts
/usr/share/fonts
$XDG_DATA_DIRS/fonts | ~/Library/Fonts
/Library/Fonts
/System/Library/Fonts
/Network/Library/Fonts | $home/lib/font
/lib/font | + +
+ +
+ Microsoft Windows +
+ +| |

Known Folder(s)

|

Fallback(s)

| +| :-----------------------------------------------------------: | :--------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------------------------------------------------------------: | +| Home | Profile | %USERPROFILE% | +| Applications | Programs
CommonPrograms | %APPDATA%\Microsoft\Windows\Start Menu\Programs
%ProgramData%\Microsoft\Windows\Start Menu\Programs | +| Fonts | Fonts
- | %SystemRoot%\Fonts
%LOCALAPPDATA%\Microsoft\Windows\Fonts | + +
+ +## Usage + +#### XDG Base Directory + +```go +package main + +import ( + "log" + + "github.com/adrg/xdg" +) + +func main() { + // XDG Base Directory paths. + log.Println("Home data directory:", xdg.DataHome) + log.Println("Data directories:", xdg.DataDirs) + log.Println("Home config directory:", xdg.ConfigHome) + log.Println("Config directories:", xdg.ConfigDirs) + log.Println("Home state directory:", xdg.StateHome) + log.Println("Cache directory:", xdg.CacheHome) + log.Println("Runtime directory:", xdg.RuntimeDir) + + // Other common directories. + log.Println("Home directory:", xdg.Home) + log.Println("Application directories:", xdg.ApplicationDirs) + log.Println("Font directories:", xdg.FontDirs) + + // Obtain a suitable location for application config files. + // ConfigFile takes one parameter which must contain the name of the file, + // but it can also contain a set of parent directories. If the directories + // don't exist, they will be created relative to the base config directory. + configFilePath, err := xdg.ConfigFile("appname/config.yaml") + if err != nil { + log.Fatal(err) + } + log.Println("Save the config file at:", configFilePath) + + // For other types of application files use: + // xdg.DataFile() + // xdg.StateFile() + // xdg.CacheFile() + // xdg.RuntimeFile() + + // Finding application config files. + // SearchConfigFile takes one parameter which must contain the name of + // the file, but it can also contain a set of parent directories relative + // to the config search paths (xdg.ConfigHome and xdg.ConfigDirs). + configFilePath, err = xdg.SearchConfigFile("appname/config.yaml") + if err != nil { + log.Fatal(err) + } + log.Println("Config file was found at:", configFilePath) + + // For other types of application files use: + // xdg.SearchDataFile() + // xdg.SearchStateFile() + // xdg.SearchCacheFile() + // xdg.SearchRuntimeFile() +} +``` + +#### XDG user directories + +```go +package main + +import ( + "log" + + "github.com/adrg/xdg" +) + +func main() { + // XDG user directories. + log.Println("Desktop directory:", xdg.UserDirs.Desktop) + log.Println("Download directory:", xdg.UserDirs.Download) + log.Println("Documents directory:", xdg.UserDirs.Documents) + log.Println("Music directory:", xdg.UserDirs.Music) + log.Println("Pictures directory:", xdg.UserDirs.Pictures) + log.Println("Videos directory:", xdg.UserDirs.Videos) + log.Println("Templates directory:", xdg.UserDirs.Templates) + log.Println("Public directory:", xdg.UserDirs.PublicShare) +} +``` + +## Stargazers over time + +[![Stargazers over time](https://starchart.cc/adrg/xdg.svg)](https://starchart.cc/adrg/xdg) + +## Contributing + +Contributions in the form of pull requests, issues or just general feedback, +are always welcome. +See [CONTRIBUTING.MD](CONTRIBUTING.md). + +**Contributors**: +[adrg](https://github.com/adrg), +[wichert](https://github.com/wichert), +[bouncepaw](https://github.com/bouncepaw), +[gabriel-vasile](https://github.com/gabriel-vasile), +[KalleDK](https://github.com/KalleDK), +[nvkv](https://github.com/nvkv), +[djdv](https://github.com/djdv). + +## References + +For more information see: +* [XDG Base Directory Specification](https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html) +* [XDG user directories](https://wiki.archlinux.org/index.php/XDG_user_directories) +* [Windows Known Folders](https://docs.microsoft.com/en-us/windows/win32/shell/knownfolderid) + +## License + +Copyright (c) 2014 Adrian-George Bostan. + +This project is licensed under the [MIT license](https://opensource.org/licenses/MIT). +See [LICENSE](LICENSE) for more details. diff --git a/vendor/github.com/adrg/xdg/base_dirs.go b/vendor/github.com/adrg/xdg/base_dirs.go new file mode 100644 index 00000000..a8a3fd55 --- /dev/null +++ b/vendor/github.com/adrg/xdg/base_dirs.go @@ -0,0 +1,68 @@ +package xdg + +import "github.com/adrg/xdg/internal/pathutil" + +// XDG Base Directory environment variables. +const ( + envDataHome = "XDG_DATA_HOME" + envDataDirs = "XDG_DATA_DIRS" + envConfigHome = "XDG_CONFIG_HOME" + envConfigDirs = "XDG_CONFIG_DIRS" + envStateHome = "XDG_STATE_HOME" + envCacheHome = "XDG_CACHE_HOME" + envRuntimeDir = "XDG_RUNTIME_DIR" +) + +type baseDirectories struct { + dataHome string + data []string + configHome string + config []string + stateHome string + cacheHome string + runtime string + + // Non-standard directories. + fonts []string + applications []string +} + +func (bd baseDirectories) dataFile(relPath string) (string, error) { + return pathutil.Create(relPath, append([]string{bd.dataHome}, bd.data...)) +} + +func (bd baseDirectories) configFile(relPath string) (string, error) { + return pathutil.Create(relPath, append([]string{bd.configHome}, bd.config...)) +} + +func (bd baseDirectories) stateFile(relPath string) (string, error) { + return pathutil.Create(relPath, []string{bd.stateHome}) +} + +func (bd baseDirectories) cacheFile(relPath string) (string, error) { + return pathutil.Create(relPath, []string{bd.cacheHome}) +} + +func (bd baseDirectories) runtimeFile(relPath string) (string, error) { + return pathutil.Create(relPath, []string{bd.runtime}) +} + +func (bd baseDirectories) searchDataFile(relPath string) (string, error) { + return pathutil.Search(relPath, append([]string{bd.dataHome}, bd.data...)) +} + +func (bd baseDirectories) searchConfigFile(relPath string) (string, error) { + return pathutil.Search(relPath, append([]string{bd.configHome}, bd.config...)) +} + +func (bd baseDirectories) searchStateFile(relPath string) (string, error) { + return pathutil.Search(relPath, []string{bd.stateHome}) +} + +func (bd baseDirectories) searchCacheFile(relPath string) (string, error) { + return pathutil.Search(relPath, []string{bd.cacheHome}) +} + +func (bd baseDirectories) searchRuntimeFile(relPath string) (string, error) { + return pathutil.Search(relPath, []string{bd.runtime}) +} diff --git a/vendor/github.com/adrg/xdg/codecov.yml b/vendor/github.com/adrg/xdg/codecov.yml new file mode 100644 index 00000000..54ee338f --- /dev/null +++ b/vendor/github.com/adrg/xdg/codecov.yml @@ -0,0 +1,11 @@ +coverage: + status: + project: + default: + target: 90% + threshold: 1% + patch: + default: + target: 100% +ignore: + - "paths_plan9.go" diff --git a/vendor/github.com/adrg/xdg/doc.go b/vendor/github.com/adrg/xdg/doc.go new file mode 100644 index 00000000..7747b183 --- /dev/null +++ b/vendor/github.com/adrg/xdg/doc.go @@ -0,0 +1,99 @@ +/* +Package xdg provides an implementation of the XDG Base Directory Specification. +The specification defines a set of standard paths for storing application files +including data and configuration files. For portability and flexibility reasons, +applications should use the XDG defined locations instead of hardcoding paths. +The package also includes the locations of well known user directories. + +The current implementation supports most flavors of Unix, Windows, Mac OS and Plan 9. + + For more information regarding the XDG Base Directory Specification see: + https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html + + For more information regarding the XDG user directories see: + https://wiki.archlinux.org/index.php/XDG_user_directories + + For more information regarding the Windows Known Folders see: + https://docs.microsoft.com/en-us/windows/win32/shell/known-folders + +Usage + +XDG Base Directory + package main + + import ( + "log" + + "github.com/adrg/xdg" + ) + + func main() { + // XDG Base Directory paths. + log.Println("Home data directory:", xdg.DataHome) + log.Println("Data directories:", xdg.DataDirs) + log.Println("Home config directory:", xdg.ConfigHome) + log.Println("Config directories:", xdg.ConfigDirs) + log.Println("Home state directory:", xdg.StateHome) + log.Println("Cache directory:", xdg.CacheHome) + log.Println("Runtime directory:", xdg.RuntimeDir) + + // Other common directories. + log.Println("Home directory:", xdg.Home) + log.Println("Application directories:", xdg.ApplicationDirs) + log.Println("Font directories:", xdg.FontDirs) + + // Obtain a suitable location for application config files. + // ConfigFile takes one parameter which must contain the name of the file, + // but it can also contain a set of parent directories. If the directories + // don't exist, they will be created relative to the base config directory. + configFilePath, err := xdg.ConfigFile("appname/config.yaml") + if err != nil { + log.Fatal(err) + } + log.Println("Save the config file at:", configFilePath) + + // For other types of application files use: + // xdg.DataFile() + // xdg.StateFile() + // xdg.CacheFile() + // xdg.RuntimeFile() + + // Finding application config files. + // SearchConfigFile takes one parameter which must contain the name of + // the file, but it can also contain a set of parent directories relative + // to the config search paths (xdg.ConfigHome and xdg.ConfigDirs). + configFilePath, err = xdg.SearchConfigFile("appname/config.yaml") + if err != nil { + log.Fatal(err) + } + log.Println("Config file was found at:", configFilePath) + + // For other types of application files use: + // xdg.SearchDataFile() + // xdg.SearchStateFile() + // xdg.SearchCacheFile() + // xdg.SearchRuntimeFile() + } + +XDG user directories + package main + + import ( + "log" + + "github.com/adrg/xdg" + ) + + func main() { + // XDG user directories. + log.Println("Desktop directory:", xdg.UserDirs.Desktop) + log.Println("Download directory:", xdg.UserDirs.Download) + log.Println("Documents directory:", xdg.UserDirs.Documents) + log.Println("Music directory:", xdg.UserDirs.Music) + log.Println("Pictures directory:", xdg.UserDirs.Pictures) + log.Println("Videos directory:", xdg.UserDirs.Videos) + log.Println("Templates directory:", xdg.UserDirs.Templates) + log.Println("Public directory:", xdg.UserDirs.PublicShare) + } +*/ +package xdg diff --git a/vendor/github.com/adrg/xdg/internal/pathutil/pathutil.go b/vendor/github.com/adrg/xdg/internal/pathutil/pathutil.go new file mode 100644 index 00000000..7422342b --- /dev/null +++ b/vendor/github.com/adrg/xdg/internal/pathutil/pathutil.go @@ -0,0 +1,78 @@ +package pathutil + +import ( + "fmt" + "os" + "path/filepath" + "strings" +) + +// Unique eliminates the duplicate paths from the provided slice and returns +// the result. The items in the output slice are in the order in which they +// occur in the input slice. If a `home` location is provided, the paths are +// expanded using the `ExpandHome` function. +func Unique(paths []string, home string) []string { + var ( + uniq []string + registry = map[string]struct{}{} + ) + + for _, p := range paths { + p = ExpandHome(p, home) + if p != "" && filepath.IsAbs(p) { + if _, ok := registry[p]; ok { + continue + } + + registry[p] = struct{}{} + uniq = append(uniq, p) + } + } + + return uniq +} + +// Create returns a suitable location relative to which the file with the +// specified `name` can be written. The first path from the provided `paths` +// slice which is successfully created (or already exists) is used as a base +// path for the file. The `name` parameter should contain the name of the file +// which is going to be written in the location returned by this function, but +// it can also contain a set of parent directories, which will be created +// relative to the selected parent path. +func Create(name string, paths []string) (string, error) { + var searchedPaths []string + for _, p := range paths { + p = filepath.Join(p, name) + + dir := filepath.Dir(p) + if Exists(dir) { + return p, nil + } + if err := os.MkdirAll(dir, os.ModeDir|0700); err == nil { + return p, nil + } + + searchedPaths = append(searchedPaths, dir) + } + + return "", fmt.Errorf("could not create any of the following paths: %s", + strings.Join(searchedPaths, ", ")) +} + +// Search searches for the file with the specified `name` in the provided +// slice of `paths`. The `name` parameter must contain the name of the file, +// but it can also contain a set of parent directories. +func Search(name string, paths []string) (string, error) { + var searchedPaths []string + for _, p := range paths { + p = filepath.Join(p, name) + if Exists(p) { + return p, nil + } + + searchedPaths = append(searchedPaths, filepath.Dir(p)) + } + + return "", fmt.Errorf("could not locate `%s` in any of the following paths: %s", + filepath.Base(name), strings.Join(searchedPaths, ", ")) +} diff --git a/vendor/github.com/adrg/xdg/internal/pathutil/pathutil_plan9.go b/vendor/github.com/adrg/xdg/internal/pathutil/pathutil_plan9.go new file mode 100644 index 00000000..8ee4e8d2 --- /dev/null +++ b/vendor/github.com/adrg/xdg/internal/pathutil/pathutil_plan9.go @@ -0,0 +1,29 @@ +package pathutil + +import ( + "os" + "path/filepath" + "strings" +) + +// Exists returns true if the specified path exists. +func Exists(path string) bool { + _, err := os.Stat(path) + return err == nil || os.IsExist(err) +} + +// ExpandHome substitutes `~` and `$home` at the start of the specified +// `path` using the provided `home` location. +func ExpandHome(path, home string) string { + if path == "" || home == "" { + return path + } + if path[0] == '~' { + return filepath.Join(home, path[1:]) + } + if strings.HasPrefix(path, "$home") { + return filepath.Join(home, path[5:]) + } + + return path +} diff --git a/vendor/github.com/adrg/xdg/internal/pathutil/pathutil_unix.go b/vendor/github.com/adrg/xdg/internal/pathutil/pathutil_unix.go new file mode 100644 index 00000000..a014c66e --- /dev/null +++ b/vendor/github.com/adrg/xdg/internal/pathutil/pathutil_unix.go @@ -0,0 +1,32 @@ +//go:build aix || darwin || dragonfly || freebsd || (js && wasm) || nacl || linux || netbsd || openbsd || solaris +// +build aix darwin dragonfly freebsd js,wasm nacl linux netbsd openbsd solaris + +package pathutil + +import ( + "os" + "path/filepath" + "strings" +) + +// Exists returns true if the specified path exists. +func Exists(path string) bool { + _, err := os.Stat(path) + return err == nil || os.IsExist(err) +} + +// ExpandHome substitutes `~` and `$HOME` at the start of the specified +// `path` using the provided `home` location. +func ExpandHome(path, home string) string { + if path == "" || home == "" { + return path + } + if path[0] == '~' { + return filepath.Join(home, path[1:]) + } + if strings.HasPrefix(path, "$HOME") { + return filepath.Join(home, path[5:]) + } + + return path +} diff --git a/vendor/github.com/adrg/xdg/internal/pathutil/pathutil_windows.go b/vendor/github.com/adrg/xdg/internal/pathutil/pathutil_windows.go new file mode 100644 index 00000000..44080e3a --- /dev/null +++ b/vendor/github.com/adrg/xdg/internal/pathutil/pathutil_windows.go @@ -0,0 +1,64 @@ +package pathutil + +import ( + "os" + "path/filepath" + "strings" + + "golang.org/x/sys/windows" +) + +// Exists returns true if the specified path exists. +func Exists(path string) bool { + fi, err := os.Lstat(path) + if fi != nil && fi.Mode()&os.ModeSymlink != 0 { + _, err = filepath.EvalSymlinks(path) + } + + return err == nil || os.IsExist(err) +} + +// ExpandHome substitutes `%USERPROFILE%` at the start of the specified +// `path` using the provided `home` location. +func ExpandHome(path, home string) string { + if path == "" || home == "" { + return path + } + if strings.HasPrefix(path, `%USERPROFILE%`) { + return filepath.Join(home, path[13:]) + } + + return path +} + +// KnownFolder returns the location of the folder with the specified ID. +// If that fails, the folder location is determined by reading the provided +// environment variables (the first non-empty read value is returned). +// If that fails as well, the first non-empty fallback is returned. +// If all of the above fails, the function returns an empty string. +func KnownFolder(id *windows.KNOWNFOLDERID, envVars []string, fallbacks []string) string { + if id != nil { + flags := []uint32{windows.KF_FLAG_DEFAULT, windows.KF_FLAG_DEFAULT_PATH} + for _, flag := range flags { + p, _ := windows.KnownFolderPath(id, flag|windows.KF_FLAG_DONT_VERIFY) + if p != "" { + return p + } + } + } + + for _, envVar := range envVars { + p := os.Getenv(envVar) + if p != "" { + return p + } + } + + for _, fallback := range fallbacks { + if fallback != "" { + return fallback + } + } + + return "" +} diff --git a/vendor/github.com/adrg/xdg/paths_darwin.go b/vendor/github.com/adrg/xdg/paths_darwin.go new file mode 100644 index 00000000..bfe9ad9b --- /dev/null +++ b/vendor/github.com/adrg/xdg/paths_darwin.go @@ -0,0 +1,60 @@ +package xdg + +import ( + "os" + "path/filepath" +) + +func homeDir() string { + if home := os.Getenv("HOME"); home != "" { + return home + } + + return "/" +} + +func initDirs(home string) { + initBaseDirs(home) + initUserDirs(home) +} + +func initBaseDirs(home string) { + homeAppSupport := filepath.Join(home, "Library", "Application Support") + rootAppSupport := "/Library/Application Support" + + // Initialize standard directories. + baseDirs.dataHome = xdgPath(envDataHome, homeAppSupport) + baseDirs.data = xdgPaths(envDataDirs, rootAppSupport) + baseDirs.configHome = xdgPath(envConfigHome, homeAppSupport) + baseDirs.config = xdgPaths(envConfigDirs, + filepath.Join(home, "Library", "Preferences"), + rootAppSupport, + "/Library/Preferences", + ) + baseDirs.stateHome = xdgPath(envStateHome, homeAppSupport) + baseDirs.cacheHome = xdgPath(envCacheHome, filepath.Join(home, "Library", "Caches")) + baseDirs.runtime = xdgPath(envRuntimeDir, homeAppSupport) + + // Initialize non-standard directories. + baseDirs.applications = []string{ + "/Applications", + } + + baseDirs.fonts = []string{ + filepath.Join(home, "Library/Fonts"), + "/Library/Fonts", + "/System/Library/Fonts", + "/Network/Library/Fonts", + } +} + +func initUserDirs(home string) { + UserDirs.Desktop = xdgPath(envDesktopDir, filepath.Join(home, "Desktop")) + UserDirs.Download = xdgPath(envDownloadDir, filepath.Join(home, "Downloads")) + UserDirs.Documents = xdgPath(envDocumentsDir, filepath.Join(home, "Documents")) + UserDirs.Music = xdgPath(envMusicDir, filepath.Join(home, "Music")) + UserDirs.Pictures = xdgPath(envPicturesDir, filepath.Join(home, "Pictures")) + UserDirs.Videos = xdgPath(envVideosDir, filepath.Join(home, "Movies")) + UserDirs.Templates = xdgPath(envTemplatesDir, filepath.Join(home, "Templates")) + UserDirs.PublicShare = xdgPath(envPublicShareDir, filepath.Join(home, "Public")) +} diff --git a/vendor/github.com/adrg/xdg/paths_plan9.go b/vendor/github.com/adrg/xdg/paths_plan9.go new file mode 100644 index 00000000..2882f688 --- /dev/null +++ b/vendor/github.com/adrg/xdg/paths_plan9.go @@ -0,0 +1,55 @@ +package xdg + +import ( + "os" + "path/filepath" +) + +func homeDir() string { + if home := os.Getenv("home"); home != "" { + return home + } + + return "/" +} + +func initDirs(home string) { + initBaseDirs(home) + initUserDirs(home) +} + +func initBaseDirs(home string) { + homeLibDir := filepath.Join(home, "lib") + rootLibDir := "/lib" + + // Initialize standard directories. + baseDirs.dataHome = xdgPath(envDataHome, homeLibDir) + baseDirs.data = xdgPaths(envDataDirs, rootLibDir) + baseDirs.configHome = xdgPath(envConfigHome, homeLibDir) + baseDirs.config = xdgPaths(envConfigDirs, rootLibDir) + baseDirs.stateHome = xdgPath(envStateHome, filepath.Join(homeLibDir, "state")) + baseDirs.cacheHome = xdgPath(envCacheHome, filepath.Join(homeLibDir, "cache")) + baseDirs.runtime = xdgPath(envRuntimeDir, "/tmp") + + // Initialize non-standard directories. + baseDirs.applications = []string{ + filepath.Join(home, "bin"), + "/bin", + } + + baseDirs.fonts = []string{ + filepath.Join(homeLibDir, "font"), + "/lib/font", + } +} + +func initUserDirs(home string) { + UserDirs.Desktop = xdgPath(envDesktopDir, filepath.Join(home, "desktop")) + UserDirs.Download = xdgPath(envDownloadDir, filepath.Join(home, "downloads")) + UserDirs.Documents = xdgPath(envDocumentsDir, filepath.Join(home, "documents")) + UserDirs.Music = xdgPath(envMusicDir, filepath.Join(home, "music")) + UserDirs.Pictures = xdgPath(envPicturesDir, filepath.Join(home, "pictures")) + UserDirs.Videos = xdgPath(envVideosDir, filepath.Join(home, "videos")) + UserDirs.Templates = xdgPath(envTemplatesDir, filepath.Join(home, "templates")) + UserDirs.PublicShare = xdgPath(envPublicShareDir, filepath.Join(home, "public")) +} diff --git a/vendor/github.com/adrg/xdg/paths_unix.go b/vendor/github.com/adrg/xdg/paths_unix.go new file mode 100644 index 00000000..ad571dfc --- /dev/null +++ b/vendor/github.com/adrg/xdg/paths_unix.go @@ -0,0 +1,71 @@ +//go:build aix || dragonfly || freebsd || (js && wasm) || nacl || linux || netbsd || openbsd || solaris +// +build aix dragonfly freebsd js,wasm nacl linux netbsd openbsd solaris + +package xdg + +import ( + "os" + "path/filepath" + "strconv" + + "github.com/adrg/xdg/internal/pathutil" +) + +func homeDir() string { + if home := os.Getenv("HOME"); home != "" { + return home + } + + return "/" +} + +func initDirs(home string) { + initBaseDirs(home) + initUserDirs(home) +} + +func initBaseDirs(home string) { + // Initialize standard directories. + baseDirs.dataHome = xdgPath(envDataHome, filepath.Join(home, ".local", "share")) + baseDirs.data = xdgPaths(envDataDirs, "/usr/local/share", "/usr/share") + baseDirs.configHome = xdgPath(envConfigHome, filepath.Join(home, ".config")) + baseDirs.config = xdgPaths(envConfigDirs, "/etc/xdg") + baseDirs.stateHome = xdgPath(envStateHome, filepath.Join(home, ".local", "state")) + baseDirs.cacheHome = xdgPath(envCacheHome, filepath.Join(home, ".cache")) + baseDirs.runtime = xdgPath(envRuntimeDir, filepath.Join("/run/user", strconv.Itoa(os.Getuid()))) + + // Initialize non-standard directories. + appDirs := []string{ + filepath.Join(baseDirs.dataHome, "applications"), + filepath.Join(home, ".local/share/applications"), + "/usr/local/share/applications", + "/usr/share/applications", + } + + fontDirs := []string{ + filepath.Join(baseDirs.dataHome, "fonts"), + filepath.Join(home, ".fonts"), + filepath.Join(home, ".local/share/fonts"), + "/usr/local/share/fonts", + "/usr/share/fonts", + } + + for _, dir := range baseDirs.data { + appDirs = append(appDirs, filepath.Join(dir, "applications")) + fontDirs = append(fontDirs, filepath.Join(dir, "fonts")) + } + + baseDirs.applications = pathutil.Unique(appDirs, Home) + baseDirs.fonts = pathutil.Unique(fontDirs, Home) +} + +func initUserDirs(home string) { + UserDirs.Desktop = xdgPath(envDesktopDir, filepath.Join(home, "Desktop")) + UserDirs.Download = xdgPath(envDownloadDir, filepath.Join(home, "Downloads")) + UserDirs.Documents = xdgPath(envDocumentsDir, filepath.Join(home, "Documents")) + UserDirs.Music = xdgPath(envMusicDir, filepath.Join(home, "Music")) + UserDirs.Pictures = xdgPath(envPicturesDir, filepath.Join(home, "Pictures")) + UserDirs.Videos = xdgPath(envVideosDir, filepath.Join(home, "Videos")) + UserDirs.Templates = xdgPath(envTemplatesDir, filepath.Join(home, "Templates")) + UserDirs.PublicShare = xdgPath(envPublicShareDir, filepath.Join(home, "Public")) +} diff --git a/vendor/github.com/adrg/xdg/paths_windows.go b/vendor/github.com/adrg/xdg/paths_windows.go new file mode 100644 index 00000000..722d3e78 --- /dev/null +++ b/vendor/github.com/adrg/xdg/paths_windows.go @@ -0,0 +1,168 @@ +package xdg + +import ( + "path/filepath" + + "github.com/adrg/xdg/internal/pathutil" + "golang.org/x/sys/windows" +) + +func homeDir() string { + return pathutil.KnownFolder( + windows.FOLDERID_Profile, + []string{"USERPROFILE"}, + nil, + ) +} + +func initDirs(home string) { + kf := initKnownFolders(home) + initBaseDirs(home, kf) + initUserDirs(home, kf) +} + +func initBaseDirs(home string, kf *knownFolders) { + // Initialize standard directories. + baseDirs.dataHome = xdgPath(envDataHome, kf.localAppData) + baseDirs.data = xdgPaths(envDataDirs, kf.roamingAppData, kf.programData) + baseDirs.configHome = xdgPath(envConfigHome, kf.localAppData) + baseDirs.config = xdgPaths(envConfigDirs, kf.programData, kf.roamingAppData) + baseDirs.stateHome = xdgPath(envStateHome, kf.localAppData) + baseDirs.cacheHome = xdgPath(envCacheHome, filepath.Join(kf.localAppData, "cache")) + baseDirs.runtime = xdgPath(envRuntimeDir, kf.localAppData) + + // Initialize non-standard directories. + baseDirs.applications = []string{ + kf.programs, + kf.commonPrograms, + } + baseDirs.fonts = []string{ + kf.fonts, + filepath.Join(kf.localAppData, "Microsoft", "Windows", "Fonts"), + } +} + +func initUserDirs(home string, kf *knownFolders) { + UserDirs.Desktop = xdgPath(envDesktopDir, kf.desktop) + UserDirs.Download = xdgPath(envDownloadDir, kf.downloads) + UserDirs.Documents = xdgPath(envDocumentsDir, kf.documents) + UserDirs.Music = xdgPath(envMusicDir, kf.music) + UserDirs.Pictures = xdgPath(envPicturesDir, kf.pictures) + UserDirs.Videos = xdgPath(envVideosDir, kf.videos) + UserDirs.Templates = xdgPath(envTemplatesDir, kf.templates) + UserDirs.PublicShare = xdgPath(envPublicShareDir, kf.public) +} + +type knownFolders struct { + systemDrive string + systemRoot string + programData string + userProfile string + userProfiles string + roamingAppData string + localAppData string + desktop string + downloads string + documents string + music string + pictures string + videos string + templates string + public string + fonts string + programs string + commonPrograms string +} + +func initKnownFolders(home string) *knownFolders { + kf := &knownFolders{ + userProfile: home, + } + kf.systemDrive = filepath.VolumeName(pathutil.KnownFolder( + windows.FOLDERID_Windows, + []string{"SystemDrive", "SystemRoot", "windir"}, + []string{home, `C:`}, + )) + string(filepath.Separator) + kf.systemRoot = pathutil.KnownFolder( + windows.FOLDERID_Windows, + []string{"SystemRoot", "windir"}, + []string{filepath.Join(kf.systemDrive, "Windows")}, + ) + kf.programData = pathutil.KnownFolder( + windows.FOLDERID_ProgramData, + []string{"ProgramData", "ALLUSERSPROFILE"}, + []string{filepath.Join(kf.systemDrive, "ProgramData")}, + ) + kf.userProfiles = pathutil.KnownFolder( + windows.FOLDERID_UserProfiles, + nil, + []string{filepath.Join(kf.systemDrive, "Users")}, + ) + kf.roamingAppData = pathutil.KnownFolder( + windows.FOLDERID_RoamingAppData, + []string{"APPDATA"}, + []string{filepath.Join(home, "AppData", "Roaming")}, + ) + kf.localAppData = pathutil.KnownFolder( + windows.FOLDERID_LocalAppData, + []string{"LOCALAPPDATA"}, + []string{filepath.Join(home, "AppData", "Local")}, + ) + kf.desktop = pathutil.KnownFolder( + windows.FOLDERID_Desktop, + nil, + []string{filepath.Join(home, "Desktop")}, + ) + kf.downloads = pathutil.KnownFolder( + windows.FOLDERID_Downloads, + nil, + []string{filepath.Join(home, "Downloads")}, + ) + kf.documents = pathutil.KnownFolder( + windows.FOLDERID_Documents, + nil, + []string{filepath.Join(home, "Documents")}, + ) + kf.music = pathutil.KnownFolder( + windows.FOLDERID_Music, + nil, + []string{filepath.Join(home, "Music")}, + ) + kf.pictures = pathutil.KnownFolder( + windows.FOLDERID_Pictures, + nil, + []string{filepath.Join(home, "Pictures")}, + ) + kf.videos = pathutil.KnownFolder( + windows.FOLDERID_Videos, + nil, + []string{filepath.Join(home, "Videos")}, + ) + kf.templates = pathutil.KnownFolder( + windows.FOLDERID_Templates, + nil, + []string{filepath.Join(kf.roamingAppData, "Microsoft", "Windows", "Templates")}, + ) + kf.public = pathutil.KnownFolder( + windows.FOLDERID_Public, + []string{"PUBLIC"}, + []string{filepath.Join(kf.userProfiles, "Public")}, + ) + kf.fonts = pathutil.KnownFolder( + windows.FOLDERID_Fonts, + nil, + []string{filepath.Join(kf.systemRoot, "Fonts")}, + ) + kf.programs = pathutil.KnownFolder( + windows.FOLDERID_Programs, + nil, + []string{filepath.Join(kf.roamingAppData, "Microsoft", "Windows", "Start Menu", "Programs")}, + ) + kf.commonPrograms = pathutil.KnownFolder( + windows.FOLDERID_CommonPrograms, + nil, + []string{filepath.Join(kf.programData, "Microsoft", "Windows", "Start Menu", "Programs")}, + ) + + return kf +} diff --git a/vendor/github.com/adrg/xdg/user_dirs.go b/vendor/github.com/adrg/xdg/user_dirs.go new file mode 100644 index 00000000..72088748 --- /dev/null +++ b/vendor/github.com/adrg/xdg/user_dirs.go @@ -0,0 +1,40 @@ +package xdg + +// XDG user directories environment variables. +const ( + envDesktopDir = "XDG_DESKTOP_DIR" + envDownloadDir = "XDG_DOWNLOAD_DIR" + envDocumentsDir = "XDG_DOCUMENTS_DIR" + envMusicDir = "XDG_MUSIC_DIR" + envPicturesDir = "XDG_PICTURES_DIR" + envVideosDir = "XDG_VIDEOS_DIR" + envTemplatesDir = "XDG_TEMPLATES_DIR" + envPublicShareDir = "XDG_PUBLICSHARE_DIR" +) + +// UserDirectories defines the locations of well known user directories. +type UserDirectories struct { + // Desktop defines the location of the user's desktop directory. + Desktop string + + // Download defines a suitable location for user downloaded files. + Download string + + // Documents defines a suitable location for user document files. + Documents string + + // Music defines a suitable location for user audio files. + Music string + + // Pictures defines a suitable location for user image files. + Pictures string + + // VideosDir defines a suitable location for user video files. + Videos string + + // Templates defines a suitable location for user template files. + Templates string + + // PublicShare defines a suitable location for user shared files. + PublicShare string +} diff --git a/vendor/github.com/adrg/xdg/xdg.go b/vendor/github.com/adrg/xdg/xdg.go new file mode 100644 index 00000000..3d33ca6e --- /dev/null +++ b/vendor/github.com/adrg/xdg/xdg.go @@ -0,0 +1,218 @@ +package xdg + +import ( + "os" + "path/filepath" + + "github.com/adrg/xdg/internal/pathutil" +) + +var ( + // Home contains the path of the user's home directory. + Home string + + // DataHome defines the base directory relative to which user-specific + // data files should be stored. This directory is defined by the + // $XDG_DATA_HOME environment variable. If the variable is not set, + // a default equal to $HOME/.local/share should be used. + DataHome string + + // DataDirs defines the preference-ordered set of base directories to + // search for data files in addition to the DataHome base directory. + // This set of directories is defined by the $XDG_DATA_DIRS environment + // variable. If the variable is not set, the default directories + // to be used are /usr/local/share and /usr/share, in that order. The + // DataHome directory is considered more important than any of the + // directories defined by DataDirs. Therefore, user data files should be + // written relative to the DataHome directory, if possible. + DataDirs []string + + // ConfigHome defines the base directory relative to which user-specific + // configuration files should be written. This directory is defined by + // the $XDG_CONFIG_HOME environment variable. If the variable is not + // not set, a default equal to $HOME/.config should be used. + ConfigHome string + + // ConfigDirs defines the preference-ordered set of base directories to + // search for configuration files in addition to the ConfigHome base + // directory. This set of directories is defined by the $XDG_CONFIG_DIRS + // environment variable. If the variable is not set, a default equal + // to /etc/xdg should be used. The ConfigHome directory is considered + // more important than any of the directories defined by ConfigDirs. + // Therefore, user config files should be written relative to the + // ConfigHome directory, if possible. + ConfigDirs []string + + // StateHome defines the base directory relative to which user-specific + // state files should be stored. This directory is defined by the + // $XDG_STATE_HOME environment variable. If the variable is not set, + // a default equal to ~/.local/state should be used. + StateHome string + + // CacheHome defines the base directory relative to which user-specific + // non-essential (cached) data should be written. This directory is + // defined by the $XDG_CACHE_HOME environment variable. If the variable + // is not set, a default equal to $HOME/.cache should be used. + CacheHome string + + // RuntimeDir defines the base directory relative to which user-specific + // non-essential runtime files and other file objects (such as sockets, + // named pipes, etc.) should be stored. This directory is defined by the + // $XDG_RUNTIME_DIR environment variable. If the variable is not set, + // applications should fall back to a replacement directory with similar + // capabilities. Applications should use this directory for communication + // and synchronization purposes and should not place larger files in it, + // since it might reside in runtime memory and cannot necessarily be + // swapped out to disk. + RuntimeDir string + + // UserDirs defines the locations of well known user directories. + UserDirs UserDirectories + + // FontDirs defines the common locations where font files are stored. + FontDirs []string + + // ApplicationDirs defines the common locations of applications. + ApplicationDirs []string + + // baseDirs defines the locations of base directories. + baseDirs baseDirectories +) + +func init() { + Reload() +} + +// Reload refreshes base and user directories by reading the environment. +// Defaults are applied for XDG variables which are empty or not present +// in the environment. +func Reload() { + // Initialize home directory. + Home = homeDir() + + // Initialize base and user directories. + initDirs(Home) + + // Set standard directories. + DataHome = baseDirs.dataHome + DataDirs = baseDirs.data + ConfigHome = baseDirs.configHome + ConfigDirs = baseDirs.config + StateHome = baseDirs.stateHome + CacheHome = baseDirs.cacheHome + RuntimeDir = baseDirs.runtime + + // Set non-standard directories. + FontDirs = baseDirs.fonts + ApplicationDirs = baseDirs.applications +} + +// DataFile returns a suitable location for the specified data file. +// The relPath parameter must contain the name of the data file, and +// optionally, a set of parent directories (e.g. appname/app.data). +// If the specified directories do not exist, they will be created relative +// to the base data directory. On failure, an error containing the +// attempted paths is returned. +func DataFile(relPath string) (string, error) { + return baseDirs.dataFile(relPath) +} + +// ConfigFile returns a suitable location for the specified config file. +// The relPath parameter must contain the name of the config file, and +// optionally, a set of parent directories (e.g. appname/app.yaml). +// If the specified directories do not exist, they will be created relative +// to the base config directory. On failure, an error containing the +// attempted paths is returned. +func ConfigFile(relPath string) (string, error) { + return baseDirs.configFile(relPath) +} + +// StateFile returns a suitable location for the specified state file. State +// files are usually volatile data files, not suitable to be stored relative +// to the $XDG_DATA_HOME directory. +// The relPath parameter must contain the name of the state file, and +// optionally, a set of parent directories (e.g. appname/app.state). +// If the specified directories do not exist, they will be created relative +// to the base state directory. On failure, an error containing the +// attempted paths is returned. +func StateFile(relPath string) (string, error) { + return baseDirs.stateFile(relPath) +} + +// CacheFile returns a suitable location for the specified cache file. +// The relPath parameter must contain the name of the cache file, and +// optionally, a set of parent directories (e.g. appname/app.cache). +// If the specified directories do not exist, they will be created relative +// to the base cache directory. On failure, an error containing the +// attempted paths is returned. +func CacheFile(relPath string) (string, error) { + return baseDirs.cacheFile(relPath) +} + +// RuntimeFile returns a suitable location for the specified runtime file. +// The relPath parameter must contain the name of the runtime file, and +// optionally, a set of parent directories (e.g. appname/app.pid). +// If the specified directories do not exist, they will be created relative +// to the base runtime directory. On failure, an error containing the +// attempted paths is returned. +func RuntimeFile(relPath string) (string, error) { + return baseDirs.runtimeFile(relPath) +} + +// SearchDataFile searches for specified file in the data search paths. +// The relPath parameter must contain the name of the data file, and +// optionally, a set of parent directories (e.g. appname/app.data). If the +// file cannot be found, an error specifying the searched paths is returned. +func SearchDataFile(relPath string) (string, error) { + return baseDirs.searchDataFile(relPath) +} + +// SearchConfigFile searches for the specified file in config search paths. +// The relPath parameter must contain the name of the config file, and +// optionally, a set of parent directories (e.g. appname/app.yaml). If the +// file cannot be found, an error specifying the searched paths is returned. +func SearchConfigFile(relPath string) (string, error) { + return baseDirs.searchConfigFile(relPath) +} + +// SearchStateFile searches for the specified file in the state search path. +// The relPath parameter must contain the name of the state file, and +// optionally, a set of parent directories (e.g. appname/app.state). If the +// file cannot be found, an error specifying the searched path is returned. +func SearchStateFile(relPath string) (string, error) { + return baseDirs.searchStateFile(relPath) +} + +// SearchCacheFile searches for the specified file in the cache search path. +// The relPath parameter must contain the name of the cache file, and +// optionally, a set of parent directories (e.g. appname/app.cache). If the +// file cannot be found, an error specifying the searched path is returned. +func SearchCacheFile(relPath string) (string, error) { + return baseDirs.searchCacheFile(relPath) +} + +// SearchRuntimeFile searches for the specified file in the runtime search path. +// The relPath parameter must contain the name of the runtime file, and +// optionally, a set of parent directories (e.g. appname/app.pid). If the +// file cannot be found, an error specifying the searched path is returned. +func SearchRuntimeFile(relPath string) (string, error) { + return baseDirs.searchRuntimeFile(relPath) +} + +func xdgPath(name, defaultPath string) string { + dir := pathutil.ExpandHome(os.Getenv(name), Home) + if dir != "" && filepath.IsAbs(dir) { + return dir + } + + return defaultPath +} + +func xdgPaths(name string, defaultPaths ...string) []string { + dirs := pathutil.Unique(filepath.SplitList(os.Getenv(name)), Home) + if len(dirs) != 0 { + return dirs + } + + return pathutil.Unique(defaultPaths, Home) +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 61e8b1a7..c5d5f511 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -1,3 +1,7 @@ +# github.com/adrg/xdg v0.4.0 +## explicit; go 1.14 +github.com/adrg/xdg +github.com/adrg/xdg/internal/pathutil # github.com/alecthomas/chroma v0.10.0 ## explicit; go 1.13 github.com/alecthomas/chroma From 284817997a998909808a5f0e59400518902bad4b Mon Sep 17 00:00:00 2001 From: Mike Zorn Date: Thu, 27 Jun 2024 11:20:12 -0700 Subject: [PATCH 42/79] feat: add ui routes (#348) * Use go generate instead of make * Add ui asset handler --- internal/dev_server/api/Makefile | 2 -- internal/dev_server/api/server.go | 1 + internal/dev_server/dev_server.go | 3 +++ internal/dev_server/ui/asset_handler.go | 11 +++++++++++ 4 files changed, 15 insertions(+), 2 deletions(-) delete mode 100644 internal/dev_server/api/Makefile create mode 100644 internal/dev_server/ui/asset_handler.go diff --git a/internal/dev_server/api/Makefile b/internal/dev_server/api/Makefile deleted file mode 100644 index 8989ff27..00000000 --- a/internal/dev_server/api/Makefile +++ /dev/null @@ -1,2 +0,0 @@ -server.gen.go: api.yaml oapi-codegen-cfg.yaml - oapi-codegen -config oapi-codegen-cfg.yaml api.yaml \ No newline at end of file diff --git a/internal/dev_server/api/server.go b/internal/dev_server/api/server.go index b3dae771..44b00b1a 100644 --- a/internal/dev_server/api/server.go +++ b/internal/dev_server/api/server.go @@ -8,6 +8,7 @@ import ( "github.com/launchdarkly/ldcli/internal/dev_server/model" ) +//go:generate oapi-codegen -config oapi-codegen-cfg.yaml api.yaml type Server struct { } diff --git a/internal/dev_server/dev_server.go b/internal/dev_server/dev_server.go index 5d189f3c..c32059c4 100644 --- a/internal/dev_server/dev_server.go +++ b/internal/dev_server/dev_server.go @@ -16,6 +16,7 @@ import ( "github.com/launchdarkly/ldcli/internal/dev_server/db" "github.com/launchdarkly/ldcli/internal/dev_server/model" "github.com/launchdarkly/ldcli/internal/dev_server/sdk" + "github.com/launchdarkly/ldcli/internal/dev_server/ui" ) type Client interface { @@ -49,6 +50,8 @@ func (c LDClient) RunServer(ctx context.Context, accessToken, baseURI string) { r.Use(adapters.Middleware(*ldClient, "https://relay-stg.ld.catamorphic.com")) // TODO add to config r.Use(model.StoreMiddleware(sqlStore)) r.Use(model.ObserversMiddleware(model.NewObservers())) + r.Handle("/ui", http.RedirectHandler("/ui/", http.StatusMovedPermanently)) + r.PathPrefix("/ui/").Handler(http.StripPrefix("/ui/", ui.AssetHandler)) sdk.BindRoutes(r) handler := api.HandlerFromMux(apiServer, r) handler = handlers.CombinedLoggingHandler(os.Stdout, handler) diff --git a/internal/dev_server/ui/asset_handler.go b/internal/dev_server/ui/asset_handler.go new file mode 100644 index 00000000..aabbed07 --- /dev/null +++ b/internal/dev_server/ui/asset_handler.go @@ -0,0 +1,11 @@ +package ui + +import ( + "embed" + "net/http" +) + +//go:embed index.html +var content embed.FS + +var AssetHandler = http.FileServerFS(content) From d8ac832a930583b71da32acc30073e13fd801a21 Mon Sep 17 00:00:00 2001 From: Kerrie Martinez Date: Thu, 27 Jun 2024 11:25:13 -0700 Subject: [PATCH 43/79] Adds a sync api (#346) * adding sync project api * adding expand to post * updating cli command to match actual api * minor refactor --- cmd/dev_server/projects.go | 4 +- internal/dev_server/api/api.yaml | 30 +++++- internal/dev_server/api/server.gen.go | 149 +++++++++++++++++++++++++- internal/dev_server/api/server.go | 64 +++++++++-- internal/dev_server/db/sqlite.go | 23 ++++ internal/dev_server/model/project.go | 49 +++++++-- internal/dev_server/model/store.go | 1 + 7 files changed, 298 insertions(+), 22 deletions(-) diff --git a/cmd/dev_server/projects.go b/cmd/dev_server/projects.go index 20879586..559a7da0 100644 --- a/cmd/dev_server/projects.go +++ b/cmd/dev_server/projects.go @@ -111,9 +111,9 @@ func NewSyncProjectCmd(client dev_server.LocalClient) *cobra.Command { func syncProject(client dev_server.LocalClient) func(*cobra.Command, []string) error { return func(cmd *cobra.Command, args []string) error { - path := DEV_SERVER + "/dev/projects/" + viper.GetString(cliflags.ProjectFlag) + "sync" + path := DEV_SERVER + "/dev/projects/" + viper.GetString(cliflags.ProjectFlag) + "/sync" res, err := client.MakeRequest( - "PUT", + "PATCH", path, nil, ) diff --git a/internal/dev_server/api/api.yaml b/internal/dev_server/api/api.yaml index df162564..77574767 100644 --- a/internal/dev_server/api/api.yaml +++ b/internal/dev_server/api/api.yaml @@ -23,6 +23,25 @@ paths: items: type: string uniqueItems: true + /dev/projects/{projectKey}/sync: + patch: + summary: updates the flag state for the given project and source environment + parameters: + - $ref: "#/components/parameters/projectKey" + - name: expand + description: Available expand options for this endpoint. + in: query + schema: + type: array + items: + type: string + enum: + - overrides + responses: + 200: + $ref: "#/components/responses/Project" + 404: + description: No project found /dev/projects/{projectKey}: get: summary: get the specified project and its configuration for syncing from the LaunchDarkly Service @@ -56,6 +75,15 @@ paths: summary: Add the project to the dev server parameters: - $ref: "#/components/parameters/projectKey" + - name: expand + description: Available expand options for this endpoint. + in: query + schema: + type: array + items: + type: string + enum: + - overrides requestBody: content: application/json: @@ -177,7 +205,7 @@ components: overrides: type: object description: flags and their values and version for a given project in the source environment - x-go-type: model.FlagsState + x-go-type: model.Overrides x-go-type-import: path: github.com/launchdarkly/ldcli/internal/dev_server/model _lastSyncedFromSource: diff --git a/internal/dev_server/api/server.gen.go b/internal/dev_server/api/server.gen.go index b1d0da1b..75523867 100644 --- a/internal/dev_server/api/server.gen.go +++ b/internal/dev_server/api/server.gen.go @@ -19,7 +19,17 @@ import ( // Defines values for GetDevProjectsProjectKeyParamsExpand. const ( - Overrides GetDevProjectsProjectKeyParamsExpand = "overrides" + GetDevProjectsProjectKeyParamsExpandOverrides GetDevProjectsProjectKeyParamsExpand = "overrides" +) + +// Defines values for PostDevProjectsProjectKeyParamsExpand. +const ( + PostDevProjectsProjectKeyParamsExpandOverrides PostDevProjectsProjectKeyParamsExpand = "overrides" +) + +// Defines values for PatchDevProjectsProjectKeySyncParamsExpand. +const ( + Overrides PatchDevProjectsProjectKeySyncParamsExpand = "overrides" ) // FlagValue value of a feature flag variation @@ -37,7 +47,7 @@ type Project struct { FlagsState *model.FlagsState `json:"flagsState,omitempty"` // Overrides flags and their values and version for a given project in the source environment - Overrides *model.FlagsState `json:"overrides,omitempty"` + Overrides *model.Overrides `json:"overrides,omitempty"` // SourceEnvironmentKey environment to copy flag values from SourceEnvironmentKey string `json:"sourceEnvironmentKey"` @@ -73,6 +83,24 @@ type PostDevProjectsProjectKeyJSONBody struct { SourceEnvironmentKey string `json:"sourceEnvironmentKey"` } +// PostDevProjectsProjectKeyParams defines parameters for PostDevProjectsProjectKey. +type PostDevProjectsProjectKeyParams struct { + // Expand Available expand options for this endpoint. + Expand *[]PostDevProjectsProjectKeyParamsExpand `form:"expand,omitempty" json:"expand,omitempty"` +} + +// PostDevProjectsProjectKeyParamsExpand defines parameters for PostDevProjectsProjectKey. +type PostDevProjectsProjectKeyParamsExpand string + +// PatchDevProjectsProjectKeySyncParams defines parameters for PatchDevProjectsProjectKeySync. +type PatchDevProjectsProjectKeySyncParams struct { + // Expand Available expand options for this endpoint. + Expand *[]PatchDevProjectsProjectKeySyncParamsExpand `form:"expand,omitempty" json:"expand,omitempty"` +} + +// PatchDevProjectsProjectKeySyncParamsExpand defines parameters for PatchDevProjectsProjectKeySync. +type PatchDevProjectsProjectKeySyncParamsExpand string + // PostDevProjectsProjectKeyJSONRequestBody defines body for PostDevProjectsProjectKey for application/json ContentType. type PostDevProjectsProjectKeyJSONRequestBody PostDevProjectsProjectKeyJSONBody @@ -92,13 +120,16 @@ type ServerInterface interface { GetDevProjectsProjectKey(w http.ResponseWriter, r *http.Request, projectKey ProjectKey, params GetDevProjectsProjectKeyParams) // Add the project to the dev server // (POST /dev/projects/{projectKey}) - PostDevProjectsProjectKey(w http.ResponseWriter, r *http.Request, projectKey ProjectKey) + PostDevProjectsProjectKey(w http.ResponseWriter, r *http.Request, projectKey ProjectKey, params PostDevProjectsProjectKeyParams) // remove override for flag // (DELETE /dev/projects/{projectKey}/overrides/{flagKey}) DeleteDevProjectsProjectKeyOverridesFlagKey(w http.ResponseWriter, r *http.Request, projectKey ProjectKey, flagKey FlagKey) // override flag value with value provided in the body // (PUT /dev/projects/{projectKey}/overrides/{flagKey}) PutDevProjectsProjectKeyOverridesFlagKey(w http.ResponseWriter, r *http.Request, projectKey ProjectKey, flagKey FlagKey) + // updates the flag state for the given project and source environment + // (PATCH /dev/projects/{projectKey}/sync) + PatchDevProjectsProjectKeySync(w http.ResponseWriter, r *http.Request, projectKey ProjectKey, params PatchDevProjectsProjectKeySyncParams) } // ServerInterfaceWrapper converts contexts to parameters. @@ -203,8 +234,19 @@ func (siw *ServerInterfaceWrapper) PostDevProjectsProjectKey(w http.ResponseWrit return } + // Parameter object where we will unmarshal all parameters from the context + var params PostDevProjectsProjectKeyParams + + // ------------- Optional query parameter "expand" ------------- + + err = runtime.BindQueryParameter("form", true, false, "expand", r.URL.Query(), ¶ms.Expand) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "expand", Err: err}) + return + } + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - siw.Handler.PostDevProjectsProjectKey(w, r, projectKey) + siw.Handler.PostDevProjectsProjectKey(w, r, projectKey, params) })) for _, middleware := range siw.HandlerMiddlewares { @@ -284,6 +326,43 @@ func (siw *ServerInterfaceWrapper) PutDevProjectsProjectKeyOverridesFlagKey(w ht handler.ServeHTTP(w, r.WithContext(ctx)) } +// PatchDevProjectsProjectKeySync operation middleware +func (siw *ServerInterfaceWrapper) PatchDevProjectsProjectKeySync(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + var err error + + // ------------- Path parameter "projectKey" ------------- + var projectKey ProjectKey + + err = runtime.BindStyledParameterWithOptions("simple", "projectKey", mux.Vars(r)["projectKey"], &projectKey, runtime.BindStyledParameterOptions{Explode: false, Required: true}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "projectKey", Err: err}) + return + } + + // Parameter object where we will unmarshal all parameters from the context + var params PatchDevProjectsProjectKeySyncParams + + // ------------- Optional query parameter "expand" ------------- + + err = runtime.BindQueryParameter("form", true, false, "expand", r.URL.Query(), ¶ms.Expand) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "expand", Err: err}) + return + } + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.PatchDevProjectsProjectKeySync(w, r, projectKey, params) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r.WithContext(ctx)) +} + type UnescapedCookieParamError struct { ParamName string Err error @@ -409,6 +488,8 @@ func HandlerWithOptions(si ServerInterface, options GorillaServerOptions) http.H r.HandleFunc(options.BaseURL+"/dev/projects/{projectKey}/overrides/{flagKey}", wrapper.PutDevProjectsProjectKeyOverridesFlagKey).Methods("PUT") + r.HandleFunc(options.BaseURL+"/dev/projects/{projectKey}/sync", wrapper.PatchDevProjectsProjectKeySync).Methods("PATCH") + return r } @@ -490,6 +571,7 @@ func (response GetDevProjectsProjectKey404Response) VisitGetDevProjectsProjectKe type PostDevProjectsProjectKeyRequestObject struct { ProjectKey ProjectKey `json:"projectKey"` + Params PostDevProjectsProjectKeyParams Body *PostDevProjectsProjectKeyJSONRequestBody } @@ -550,6 +632,32 @@ func (response PutDevProjectsProjectKeyOverridesFlagKey200JSONResponse) VisitPut return json.NewEncoder(w).Encode(response) } +type PatchDevProjectsProjectKeySyncRequestObject struct { + ProjectKey ProjectKey `json:"projectKey"` + Params PatchDevProjectsProjectKeySyncParams +} + +type PatchDevProjectsProjectKeySyncResponseObject interface { + VisitPatchDevProjectsProjectKeySyncResponse(w http.ResponseWriter) error +} + +type PatchDevProjectsProjectKeySync200JSONResponse struct{ ProjectJSONResponse } + +func (response PatchDevProjectsProjectKeySync200JSONResponse) VisitPatchDevProjectsProjectKeySyncResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type PatchDevProjectsProjectKeySync404Response struct { +} + +func (response PatchDevProjectsProjectKeySync404Response) VisitPatchDevProjectsProjectKeySyncResponse(w http.ResponseWriter) error { + w.WriteHeader(404) + return nil +} + // StrictServerInterface represents all server handlers. type StrictServerInterface interface { // lists all projects that have been configured for the dev server @@ -570,6 +678,9 @@ type StrictServerInterface interface { // override flag value with value provided in the body // (PUT /dev/projects/{projectKey}/overrides/{flagKey}) PutDevProjectsProjectKeyOverridesFlagKey(ctx context.Context, request PutDevProjectsProjectKeyOverridesFlagKeyRequestObject) (PutDevProjectsProjectKeyOverridesFlagKeyResponseObject, error) + // updates the flag state for the given project and source environment + // (PATCH /dev/projects/{projectKey}/sync) + PatchDevProjectsProjectKeySync(ctx context.Context, request PatchDevProjectsProjectKeySyncRequestObject) (PatchDevProjectsProjectKeySyncResponseObject, error) } type StrictHandlerFunc = strictnethttp.StrictHTTPHandlerFunc @@ -679,10 +790,11 @@ func (sh *strictHandler) GetDevProjectsProjectKey(w http.ResponseWriter, r *http } // PostDevProjectsProjectKey operation middleware -func (sh *strictHandler) PostDevProjectsProjectKey(w http.ResponseWriter, r *http.Request, projectKey ProjectKey) { +func (sh *strictHandler) PostDevProjectsProjectKey(w http.ResponseWriter, r *http.Request, projectKey ProjectKey, params PostDevProjectsProjectKeyParams) { var request PostDevProjectsProjectKeyRequestObject request.ProjectKey = projectKey + request.Params = params var body PostDevProjectsProjectKeyJSONRequestBody if err := json.NewDecoder(r.Body).Decode(&body); err != nil { @@ -771,3 +883,30 @@ func (sh *strictHandler) PutDevProjectsProjectKeyOverridesFlagKey(w http.Respons sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response)) } } + +// PatchDevProjectsProjectKeySync operation middleware +func (sh *strictHandler) PatchDevProjectsProjectKeySync(w http.ResponseWriter, r *http.Request, projectKey ProjectKey, params PatchDevProjectsProjectKeySyncParams) { + var request PatchDevProjectsProjectKeySyncRequestObject + + request.ProjectKey = projectKey + request.Params = params + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.PatchDevProjectsProjectKeySync(ctx, request.(PatchDevProjectsProjectKeySyncRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "PatchDevProjectsProjectKeySync") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(PatchDevProjectsProjectKeySyncResponseObject); ok { + if err := validResponse.VisitPatchDevProjectsProjectKeySyncResponse(w); err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } + } else if response != nil { + sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response)) + } +} diff --git a/internal/dev_server/api/server.go b/internal/dev_server/api/server.go index 44b00b1a..82e50823 100644 --- a/internal/dev_server/api/server.go +++ b/internal/dev_server/api/server.go @@ -85,17 +85,69 @@ func (s Server) GetDevProjectsProjectKey(ctx context.Context, request GetDevProj } func (s Server) PostDevProjectsProjectKey(ctx context.Context, request PostDevProjectsProjectKeyRequestObject) (PostDevProjectsProjectKeyResponseObject, error) { + store := model.StoreFromContext(ctx) project, err := model.CreateProject(ctx, request.ProjectKey, request.Body.SourceEnvironmentKey, nil) if err != nil { return nil, err } + + response := ProjectJSONResponse{ + LastSyncedFromSource: project.LastSyncTime.Unix(), + Context: project.Context, + SourceEnvironmentKey: project.SourceEnvironmentKey, + FlagsState: &project.FlagState, + } + + if request.Params.Expand != nil { + for _, item := range *request.Params.Expand { + if item == "overrides" { + overrides, err := store.GetOverridesForProject(ctx, request.ProjectKey) + if err != nil { + return nil, err + } + response.Overrides = &overrides + } + } + + } + return PostDevProjectsProjectKey201JSONResponse{ - ProjectJSONResponse{ - LastSyncedFromSource: project.LastSyncTime.Unix(), - Context: project.Context, - SourceEnvironmentKey: project.SourceEnvironmentKey, - FlagsState: &project.FlagState, - }, + response, + }, nil +} + +func (s Server) PatchDevProjectsProjectKeySync(ctx context.Context, request PatchDevProjectsProjectKeySyncRequestObject) (PatchDevProjectsProjectKeySyncResponseObject, error) { + store := model.StoreFromContext(ctx) + project, err := model.SyncProject(ctx, request.ProjectKey) + if err != nil { + return nil, err + } + if project.Key == "" && project.SourceEnvironmentKey == "" { + return PatchDevProjectsProjectKeySync404Response{}, nil + } + + response := ProjectJSONResponse{ + LastSyncedFromSource: project.LastSyncTime.Unix(), + Context: project.Context, + SourceEnvironmentKey: project.SourceEnvironmentKey, + FlagsState: &project.FlagState, + } + + if request.Params.Expand != nil { + for _, item := range *request.Params.Expand { + if item == "overrides" { + overrides, err := store.GetOverridesForProject(ctx, request.ProjectKey) + if err != nil { + return nil, err + } + response.Overrides = &overrides + } + } + + } + + return PatchDevProjectsProjectKeySync200JSONResponse{ + response, }, nil } diff --git a/internal/dev_server/db/sqlite.go b/internal/dev_server/db/sqlite.go index 0962dc5e..f4423d7e 100644 --- a/internal/dev_server/db/sqlite.go +++ b/internal/dev_server/db/sqlite.go @@ -66,6 +66,29 @@ func (s Sqlite) GetDevProject(ctx context.Context, key string) (*model.Project, return &project, nil } +func (s Sqlite) UpdateProject(ctx context.Context, project model.Project) (bool, error) { + flagsStateJson, err := json.Marshal(project.FlagState) + if err != nil { + return false, errors.Wrap(err, "unable to marshal flags state when updating project") + } + + result, err := s.database.ExecContext(ctx, ` + UPDATE projects + SET flag_state = ?, last_sync_time = ? + WHERE key = ?; + `, flagsStateJson, project.LastSyncTime, project.Key) + + rowsAffected, err := result.RowsAffected() + if err != nil { + return false, err + } + if rowsAffected == 0 { + return false, nil + } + + return true, nil +} + func (s Sqlite) DeleteDevProject(ctx context.Context, key string) (bool, error) { result, err := s.database.Exec("DELETE FROM projects where key=?", key) if err != nil { diff --git a/internal/dev_server/model/project.go b/internal/dev_server/model/project.go index cb74fb97..c6963bb2 100644 --- a/internal/dev_server/model/project.go +++ b/internal/dev_server/model/project.go @@ -19,6 +19,7 @@ type Project struct { // CreateProject creates a project and adds it to the database. func CreateProject(ctx context.Context, projectKey, sourceEnvironmentKey string, ldCtx *ldcontext.Context) (Project, error) { + store := StoreFromContext(ctx) project := Project{} project.Key = projectKey project.SourceEnvironmentKey = sourceEnvironmentKey @@ -27,26 +28,42 @@ func CreateProject(ctx context.Context, projectKey, sourceEnvironmentKey string, } else { project.Context = *ldCtx } + flagsState, err := project.FetchFlagState(ctx) + if err != nil { + return Project{}, err + } - sdkAdapter := adapters.GetSdk(ctx) - apiAdapter := adapters.GetApi(ctx) + project.FlagState = flagsState + project.LastSyncTime = time.Now() + + err = store.InsertProject(ctx, project) + if err != nil { + return Project{}, err + } + return project, nil +} + +func SyncProject(ctx context.Context, projectKey string) (Project, error) { store := StoreFromContext(ctx) - sdkKey, err := apiAdapter.GetSdkKey(ctx, projectKey, sourceEnvironmentKey) + project, err := store.GetDevProject(ctx, projectKey) if err != nil { return Project{}, err } - sdkFlags, err := sdkAdapter.GetAllFlagsState(ctx, project.Context, sdkKey) + flagsState, err := project.FetchFlagState(ctx) if err != nil { return Project{}, err } - project.FlagState = FromAllFlags(sdkFlags) - project.LastSyncTime = time.Now() - err = store.InsertProject(ctx, project) + project.FlagState = flagsState + project.LastSyncTime = time.Now() + updated, err := store.UpdateProject(ctx, *project) if err != nil { return Project{}, err } - return project, nil + if !updated { + return Project{}, err + } + return *project, nil } func (p Project) GetFlagStateWithOverridesForProject(ctx context.Context) (FlagsState, error) { @@ -64,3 +81,19 @@ func (p Project) GetFlagStateWithOverridesForProject(ctx context.Context) (Flags } return withOverrides, nil } + +func (p Project) FetchFlagState(ctx context.Context) (FlagsState, error) { + sdkAdapter := adapters.GetSdk(ctx) + apiAdapter := adapters.GetApi(ctx) + sdkKey, err := apiAdapter.GetSdkKey(ctx, p.Key, p.SourceEnvironmentKey) + flagsState := make(FlagsState) + if err != nil { + return flagsState, err + } + sdkFlags, err := sdkAdapter.GetAllFlagsState(ctx, p.Context, sdkKey) + if err != nil { + return flagsState, err + } + flagsState = FromAllFlags(sdkFlags) + return flagsState, nil +} diff --git a/internal/dev_server/model/store.go b/internal/dev_server/model/store.go index 0d9f4308..5eb0d810 100644 --- a/internal/dev_server/model/store.go +++ b/internal/dev_server/model/store.go @@ -15,6 +15,7 @@ type Store interface { DeleteOverride(ctx context.Context, projectKey, flagKey string) error GetDevProjects(ctx context.Context) ([]string, error) GetDevProject(ctx context.Context, projectKey string) (*Project, error) + UpdateProject(ctx context.Context, project Project) (bool, error) DeleteDevProject(ctx context.Context, projectKey string) (bool, error) InsertProject(ctx context.Context, project Project) error UpsertOverride(ctx context.Context, override Override) (Override, error) From 8788bbf21b927ab2952bc1b304ed995487cd286b Mon Sep 17 00:00:00 2001 From: Kelly Hofmann <55991524+k3llymariee@users.noreply.github.com> Date: Thu, 27 Jun 2024 13:17:05 -0700 Subject: [PATCH 44/79] add dev stream uri to config file (#347) * add dev stream uri to config file * actually use the value * change variable name --- cmd/cliflags/flags.go | 40 +++++++++++++++++-------------- cmd/config/testdata/help.golden | 1 + cmd/dev_server/dev_server.go | 11 +++++++++ cmd/dev_server/start_server.go | 7 +++++- internal/config/config.go | 6 +++++ internal/config/config_test.go | 13 ++++++++++ internal/dev_server/dev_server.go | 6 ++--- 7 files changed, 62 insertions(+), 22 deletions(-) diff --git a/cmd/cliflags/flags.go b/cmd/cliflags/flags.go index baab681f..75464f6a 100644 --- a/cmd/cliflags/flags.go +++ b/cmd/cliflags/flags.go @@ -1,22 +1,25 @@ package cliflags const ( - BaseURIDefault = "https://app.launchdarkly.com" + BaseURIDefault = "https://app.launchdarkly.com" + DevStreamURIDefault = "https://stream.launchdarkly.com" - AccessTokenFlag = "access-token" - AnalyticsOptOut = "analytics-opt-out" - BaseURIFlag = "base-uri" - DataFlag = "data" - EmailsFlag = "emails" - EnvironmentFlag = "environment" - FlagFlag = "flag" - OutputFlag = "output" - ProjectFlag = "project" - RoleFlag = "role" + AccessTokenFlag = "access-token" + AnalyticsOptOut = "analytics-opt-out" + BaseURIFlag = "base-uri" + DataFlag = "data" + DevStreamURIFlag = "dev-stream-uri" + EmailsFlag = "emails" + EnvironmentFlag = "environment" + FlagFlag = "flag" + OutputFlag = "output" + ProjectFlag = "project" + RoleFlag = "role" AccessTokenFlagDescription = "LaunchDarkly access token with write-level access" AnalyticsOptOutDescription = "Opt out of analytics tracking" BaseURIFlagDescription = "LaunchDarkly base URI" + DevStreamURIDescription = "URI from which the dev server will stream flag configurations" EnvironmentFlagDescription = "Default environment key" FlagFlagDescription = "Default feature flag key" OutputFlagDescription = "Command response output format in either JSON or plain text" @@ -25,12 +28,13 @@ const ( func AllFlagsHelp() map[string]string { return map[string]string{ - AccessTokenFlag: AccessTokenFlagDescription, - AnalyticsOptOut: AnalyticsOptOutDescription, - BaseURIFlag: BaseURIFlagDescription, - EnvironmentFlag: EnvironmentFlagDescription, - FlagFlag: FlagFlagDescription, - OutputFlag: OutputFlagDescription, - ProjectFlag: ProjectFlagDescription, + AccessTokenFlag: AccessTokenFlagDescription, + AnalyticsOptOut: AnalyticsOptOutDescription, + BaseURIFlag: BaseURIFlagDescription, + DevStreamURIFlag: DevStreamURIDescription, + EnvironmentFlag: EnvironmentFlagDescription, + FlagFlag: FlagFlagDescription, + OutputFlag: OutputFlagDescription, + ProjectFlag: ProjectFlagDescription, } } diff --git a/cmd/config/testdata/help.golden b/cmd/config/testdata/help.golden index ea1b4541..1cb4ce62 100644 --- a/cmd/config/testdata/help.golden +++ b/cmd/config/testdata/help.golden @@ -4,6 +4,7 @@ Supported settings: - `access-token`: LaunchDarkly access token with write-level access - `analytics-opt-out`: Opt out of analytics tracking - `base-uri`: LaunchDarkly base URI +- `dev-stream-uri`: URI from which the dev server will stream flag configurations - `environment`: Default environment key - `flag`: Default feature flag key - `output`: Command response output format in either JSON or plain text diff --git a/cmd/dev_server/dev_server.go b/cmd/dev_server/dev_server.go index 57a06f28..a1931a09 100644 --- a/cmd/dev_server/dev_server.go +++ b/cmd/dev_server/dev_server.go @@ -2,7 +2,9 @@ package dev_server import ( "github.com/spf13/cobra" + "github.com/spf13/viper" + "github.com/launchdarkly/ldcli/cmd/cliflags" resourcecmd "github.com/launchdarkly/ldcli/cmd/resources" "github.com/launchdarkly/ldcli/internal/dev_server" ) @@ -14,6 +16,13 @@ func NewDevServerCmd(localClient dev_server.LocalClient, ldClient dev_server.Cli Long: "Start and use a local development server for overriding flag values.", } + cmd.PersistentFlags().String( + cliflags.DevStreamURIFlag, + cliflags.DevStreamURIDefault, + cliflags.DevStreamURIDescription, + ) + _ = viper.BindPFlag(cliflags.DevStreamURIFlag, cmd.PersistentFlags().Lookup(cliflags.DevStreamURIFlag)) + // Add subcommands here cmd.AddGroup(&cobra.Group{ID: "projects", Title: "Project commands:"}) cmd.AddCommand(NewListProjectsCmd(localClient)) @@ -21,10 +30,12 @@ func NewDevServerCmd(localClient dev_server.LocalClient, ldClient dev_server.Cli cmd.AddCommand(NewSyncProjectCmd(localClient)) cmd.AddCommand(NewRemoveProjectCmd(localClient)) cmd.AddCommand(NewAddProjectCmd(localClient)) + cmd.AddGroup(&cobra.Group{ID: "overrides", Title: "Override commands:"}) cmd.AddCommand(NewAddOverrideCmd(localClient)) cmd.AddCommand(NewRemoveOverrideCmd(localClient)) cmd.AddGroup(&cobra.Group{ID: "server", Title: "Server commands:"}) + cmd.AddCommand(NewStartServerCmd(ldClient)) cmd.SetUsageTemplate(resourcecmd.SubcommandUsageTemplate()) diff --git a/cmd/dev_server/start_server.go b/cmd/dev_server/start_server.go index 419660c9..4dd13339 100644 --- a/cmd/dev_server/start_server.go +++ b/cmd/dev_server/start_server.go @@ -31,7 +31,12 @@ func startServer(client dev_server.Client) func(*cobra.Command, []string) error return func(cmd *cobra.Command, args []string) error { ctx := context.Background() - client.RunServer(ctx, viper.GetString(cliflags.AccessTokenFlag), viper.GetString(cliflags.BaseURIFlag)) + client.RunServer( + ctx, + viper.GetString(cliflags.AccessTokenFlag), + viper.GetString(cliflags.BaseURIFlag), + viper.GetString(cliflags.DevStreamURIFlag), + ) return nil } diff --git a/internal/config/config.go b/internal/config/config.go index 33a7955d..7da261af 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -22,6 +22,7 @@ type ConfigFile struct { AnalyticsOptOut *bool `json:"analytics-opt-out,omitempty" yaml:"analytics-opt-out,omitempty"` BaseURI string `json:"base-uri,omitempty" yaml:"base-uri,omitempty"` Flag string `json:"flag,omitempty" yaml:"flag,omitempty"` + DevStreamURI string `json:"dev-stream-uri,omitempty" yaml:"dev-stream-uri,omitempty"` Environment string `json:"environment,omitempty" yaml:"environment,omitempty"` Output string `json:"output,omitempty" yaml:"output,omitempty"` Project string `json:"project,omitempty" yaml:"project,omitempty"` @@ -49,6 +50,7 @@ func NewConfig(rawConfig map[string]interface{}) (ConfigFile, error) { accessToken string analyticsOptOut bool baseURI string + devStreamURI string environment string err error flag string @@ -83,11 +85,15 @@ func NewConfig(rawConfig map[string]interface{}) (ConfigFile, error) { if rawConfig[cliflags.ProjectFlag] != nil { project = rawConfig[cliflags.ProjectFlag].(string) } + if rawConfig[cliflags.DevStreamURIFlag] != nil { + devStreamURI = rawConfig[cliflags.DevStreamURIFlag].(string) + } return ConfigFile{ AccessToken: accessToken, AnalyticsOptOut: &analyticsOptOut, BaseURI: baseURI, + DevStreamURI: devStreamURI, Environment: environment, Flag: flag, Output: outputKind.String(), diff --git a/internal/config/config_test.go b/internal/config/config_test.go index a348fa22..3145cfd7 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -189,6 +189,19 @@ func TestNewConfig(t *testing.T) { assert.Equal(t, "test-key", configFile.Project) }) }) + + t.Run("dev-stream-uri", func(t *testing.T) { + t.Run("sets the given value", func(t *testing.T) { + rawConfig := map[string]interface{}{ + "dev-stream-uri": "http://relay.com", + } + + configFile, err := config.NewConfig(rawConfig) + + require.NoError(t, err) + assert.Equal(t, "http://relay.com", configFile.DevStreamURI) + }) + }) } func TestService_VerifyAccessToken(t *testing.T) { diff --git a/internal/dev_server/dev_server.go b/internal/dev_server/dev_server.go index c32059c4..88fc7bf8 100644 --- a/internal/dev_server/dev_server.go +++ b/internal/dev_server/dev_server.go @@ -20,7 +20,7 @@ import ( ) type Client interface { - RunServer(ctx context.Context, accessToken, baseURI string) + RunServer(ctx context.Context, accessToken, baseURI, devStreamURI string) } type LDClient struct { @@ -33,7 +33,7 @@ func NewClient(cliVersion string) LDClient { return LDClient{cliVersion: cliVersion} } -func (c LDClient) RunServer(ctx context.Context, accessToken, baseURI string) { +func (c LDClient) RunServer(ctx context.Context, accessToken, baseURI, devStreamURI string) { ldClient := client.New(accessToken, baseURI, c.cliVersion) dbPath := getDBPath() log.Printf("Using database at %s", dbPath) @@ -47,7 +47,7 @@ func (c LDClient) RunServer(ctx context.Context, accessToken, baseURI string) { ResponseErrorHandlerFunc: ResponseErrorHandler, }) r := mux.NewRouter() - r.Use(adapters.Middleware(*ldClient, "https://relay-stg.ld.catamorphic.com")) // TODO add to config + r.Use(adapters.Middleware(*ldClient, devStreamURI)) r.Use(model.StoreMiddleware(sqlStore)) r.Use(model.ObserversMiddleware(model.NewObservers())) r.Handle("/ui", http.RedirectHandler("/ui/", http.StatusMovedPermanently)) From d81e9627e4064242f342ba3ea5090ebbe6b271e5 Mon Sep 17 00:00:00 2001 From: Keegan Watkins <109103953+kwatkins-ld@users.noreply.github.com> Date: Thu, 27 Jun 2024 15:54:23 -0500 Subject: [PATCH 45/79] feat: add frontend build (#350) * feat: add frontend build * tweak fs so that the paths work out --------- Co-authored-by: Mike Zorn --- internal/dev_server/ui/.gitignore | 1 + internal/dev_server/ui/asset_handler.go | 12 +++- internal/dev_server/ui/dist/index.html | 73 ++++++++++++++++++++++++ internal/dev_server/ui/package-lock.json | 17 ++++++ internal/dev_server/ui/package.json | 1 + internal/dev_server/ui/public/vite.svg | 1 - internal/dev_server/ui/src/App.tsx | 15 +++-- internal/dev_server/ui/vite.config.ts | 29 ++++++---- 8 files changed, 130 insertions(+), 19 deletions(-) create mode 100644 internal/dev_server/ui/.gitignore create mode 100644 internal/dev_server/ui/dist/index.html delete mode 100644 internal/dev_server/ui/public/vite.svg diff --git a/internal/dev_server/ui/.gitignore b/internal/dev_server/ui/.gitignore new file mode 100644 index 00000000..05a9d0cf --- /dev/null +++ b/internal/dev_server/ui/.gitignore @@ -0,0 +1 @@ +!dist/ \ No newline at end of file diff --git a/internal/dev_server/ui/asset_handler.go b/internal/dev_server/ui/asset_handler.go index aabbed07..e3248b96 100644 --- a/internal/dev_server/ui/asset_handler.go +++ b/internal/dev_server/ui/asset_handler.go @@ -2,10 +2,18 @@ package ui import ( "embed" + "io/fs" + "log" "net/http" ) -//go:embed index.html +//go:embed dist/index.html var content embed.FS -var AssetHandler = http.FileServerFS(content) +var AssetHandler = func() http.Handler { + dist, err := fs.Sub(content, "dist") + if err != nil { + log.Fatalf("unable to open dist: %+v", err) + } + return http.FileServerFS(dist) +}() diff --git a/internal/dev_server/ui/dist/index.html b/internal/dev_server/ui/dist/index.html new file mode 100644 index 00000000..7e511b3c --- /dev/null +++ b/internal/dev_server/ui/dist/index.html @@ -0,0 +1,73 @@ + + + + + + + Vite + React + TS + + + + +
+ + diff --git a/internal/dev_server/ui/package-lock.json b/internal/dev_server/ui/package-lock.json index 72980a3b..de1937d9 100644 --- a/internal/dev_server/ui/package-lock.json +++ b/internal/dev_server/ui/package-lock.json @@ -28,6 +28,7 @@ "prettier": "3.3.2", "typescript": "5.2.2", "vite": "5.3.1", + "vite-plugin-singlefile": "2.0.2", "vite-plugin-svgr": "^4.2.0" } }, @@ -6860,6 +6861,22 @@ } } }, + "node_modules/vite-plugin-singlefile": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/vite-plugin-singlefile/-/vite-plugin-singlefile-2.0.2.tgz", + "integrity": "sha512-Z2ou6HcvED5CF0hM+vcFSaFa+klyS8RyyLxW0PbMRLnMbvzTI6ueWyxdYNFhpuXZgz/aj6+E/dHFTdEcw6gb9w==", + "dev": true, + "dependencies": { + "micromatch": "^4.0.7" + }, + "engines": { + "node": ">18.0.0" + }, + "peerDependencies": { + "rollup": "^4.18.0", + "vite": "^5.3.1" + } + }, "node_modules/vite-plugin-svgr": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/vite-plugin-svgr/-/vite-plugin-svgr-4.2.0.tgz", diff --git a/internal/dev_server/ui/package.json b/internal/dev_server/ui/package.json index 9aa4763f..dac36ec8 100644 --- a/internal/dev_server/ui/package.json +++ b/internal/dev_server/ui/package.json @@ -31,6 +31,7 @@ "prettier": "3.3.2", "typescript": "5.2.2", "vite": "5.3.1", + "vite-plugin-singlefile": "2.0.2", "vite-plugin-svgr": "^4.2.0" } } diff --git a/internal/dev_server/ui/public/vite.svg b/internal/dev_server/ui/public/vite.svg deleted file mode 100644 index e7b8dfb1..00000000 --- a/internal/dev_server/ui/public/vite.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/internal/dev_server/ui/src/App.tsx b/internal/dev_server/ui/src/App.tsx index 601e3efb..12df0b19 100644 --- a/internal/dev_server/ui/src/App.tsx +++ b/internal/dev_server/ui/src/App.tsx @@ -17,6 +17,9 @@ import './App.css'; import { useEffect, useState } from 'react'; import { Icon } from '@launchpad-ui/icons'; +const API_BASE = import.meta.env.PROD ? '' : '/api'; +const apiRoute = (pathname: string) => `${API_BASE}${pathname}` + function App() { const [flags, setFlags] = useState(null); const [overrides, setOverrides] = useState 0; const updateOverride = (flagKey: string, overrideValue: LDFlagValue) => { - fetch(`api/dev/projects/default/overrides/${flagKey}`, { + fetch(apiRoute(`/dev/projects/default/overrides/${flagKey}`), { method: 'PUT', body: JSON.stringify(overrideValue), }) @@ -47,13 +50,13 @@ function App() { setOverrides(updatedOverrides); }) - .catch((e) => { + .catch((_e) => { // todo }); }; const removeOverride = (flagKey: string, updateState: boolean = true) => { - return fetch(`api/dev/projects/default/overrides/${flagKey}`, { + return fetch(apiRoute(`/dev/projects/default/overrides/${flagKey}`), { method: 'DELETE', }) .then((res) => { @@ -70,14 +73,14 @@ function App() { setOverrides(updatedOverrides); } }) - .catch((e) => { + .catch((_e) => { // todo }); }; // Fetch flags / overrides on mount useEffect(() => { - fetch('/api/dev/projects/default?expand=overrides') + fetch(apiRoute('/dev/projects/default?expand=overrides')) .then(async (res) => { if (!res.ok) { return; // todo @@ -96,7 +99,7 @@ function App() { setFlags(sortedFlags); setOverrides(overrides); }) - .catch((e) => { + .catch((_e) => { // todo }); }, []); diff --git a/internal/dev_server/ui/vite.config.ts b/internal/dev_server/ui/vite.config.ts index 94a5936e..53390fea 100644 --- a/internal/dev_server/ui/vite.config.ts +++ b/internal/dev_server/ui/vite.config.ts @@ -1,17 +1,26 @@ -import { defineConfig } from 'vite'; +import { PluginOption, defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; import svgr from 'vite-plugin-svgr'; +import { viteSingleFile } from 'vite-plugin-singlefile'; // https://vitejs.dev/config/ -export default defineConfig({ - plugins: [react(), svgr()], - server: { - proxy: { - '/api': { - target: 'http://localhost:8765', - changeOrigin: true, - rewrite: (path: string) => path.replace(/^\/api/, ''), +export default defineConfig(({ command }) => { + const plugins: PluginOption[] = [react(), svgr()]; + + if (command === 'build') { + plugins.push(viteSingleFile()); + } + + return { + plugins, + server: { + proxy: { + '/api': { + target: 'http://localhost:8765', + changeOrigin: true, + rewrite: (path: string) => path.replace(/^\/api/, ''), + }, }, }, - }, + } }); From 3ac3aa090d8e796ade3f32f3fff21d265dfa0a6f Mon Sep 17 00:00:00 2001 From: Kerrie Martinez Date: Thu, 27 Jun 2024 14:27:56 -0700 Subject: [PATCH 46/79] fixing overrides --- internal/dev_server/api/api.yaml | 2 +- internal/dev_server/api/server.gen.go | 2 +- internal/dev_server/api/server.go | 25 +++++++++++++++++++++++-- 3 files changed, 25 insertions(+), 4 deletions(-) diff --git a/internal/dev_server/api/api.yaml b/internal/dev_server/api/api.yaml index 77574767..7e8acd8b 100644 --- a/internal/dev_server/api/api.yaml +++ b/internal/dev_server/api/api.yaml @@ -205,7 +205,7 @@ components: overrides: type: object description: flags and their values and version for a given project in the source environment - x-go-type: model.Overrides + x-go-type: model.FlagsState x-go-type-import: path: github.com/launchdarkly/ldcli/internal/dev_server/model _lastSyncedFromSource: diff --git a/internal/dev_server/api/server.gen.go b/internal/dev_server/api/server.gen.go index 75523867..2eddf415 100644 --- a/internal/dev_server/api/server.gen.go +++ b/internal/dev_server/api/server.gen.go @@ -47,7 +47,7 @@ type Project struct { FlagsState *model.FlagsState `json:"flagsState,omitempty"` // Overrides flags and their values and version for a given project in the source environment - Overrides *model.Overrides `json:"overrides,omitempty"` + Overrides *model.FlagsState `json:"overrides,omitempty"` // SourceEnvironmentKey environment to copy flag values from SourceEnvironmentKey string `json:"sourceEnvironmentKey"` diff --git a/internal/dev_server/api/server.go b/internal/dev_server/api/server.go index 82e50823..02cc918f 100644 --- a/internal/dev_server/api/server.go +++ b/internal/dev_server/api/server.go @@ -74,6 +74,7 @@ func (s Server) GetDevProjectsProjectKey(ctx context.Context, request GetDevProj Version: override.Version, } } + response.Overrides = &respOverrides } } @@ -105,7 +106,17 @@ func (s Server) PostDevProjectsProjectKey(ctx context.Context, request PostDevPr if err != nil { return nil, err } - response.Overrides = &overrides + respOverrides := make(model.FlagsState) + for _, override := range overrides { + if !override.Active { + continue + } + respOverrides[override.FlagKey] = model.FlagState{ + Value: override.Value, + Version: override.Version, + } + } + response.Overrides = &respOverrides } } @@ -140,7 +151,17 @@ func (s Server) PatchDevProjectsProjectKeySync(ctx context.Context, request Patc if err != nil { return nil, err } - response.Overrides = &overrides + respOverrides := make(model.FlagsState) + for _, override := range overrides { + if !override.Active { + continue + } + respOverrides[override.FlagKey] = model.FlagState{ + Value: override.Value, + Version: override.Version, + } + } + response.Overrides = &respOverrides } } From 9a515f571fba0cc162bef2fad738cccf4ce7ca57 Mon Sep 17 00:00:00 2001 From: Kerrie Martinez Date: Thu, 27 Jun 2024 17:38:11 -0700 Subject: [PATCH 47/79] Add project patch (#352) * adding context to post and patch * refactoring out the expand params --- internal/dev_server/api/api.yaml | 91 ++++++++-------- internal/dev_server/api/server.gen.go | 151 +++++++++++++++++++++++++- internal/dev_server/api/server.go | 37 ++++++- internal/dev_server/db/sqlite.go | 4 +- internal/dev_server/model/project.go | 33 ++++++ 5 files changed, 264 insertions(+), 52 deletions(-) diff --git a/internal/dev_server/api/api.yaml b/internal/dev_server/api/api.yaml index 7e8acd8b..74d9228c 100644 --- a/internal/dev_server/api/api.yaml +++ b/internal/dev_server/api/api.yaml @@ -28,15 +28,7 @@ paths: summary: updates the flag state for the given project and source environment parameters: - $ref: "#/components/parameters/projectKey" - - name: expand - description: Available expand options for this endpoint. - in: query - schema: - type: array - items: - type: string - enum: - - overrides + - $ref: "#/components/parameters/expand" responses: 200: $ref: "#/components/responses/Project" @@ -47,21 +39,33 @@ paths: summary: get the specified project and its configuration for syncing from the LaunchDarkly Service parameters: - $ref: "#/components/parameters/projectKey" - - name: expand - description: Available expand options for this endpoint. - in: query - schema: - type: array - items: - type: string - enum: - - overrides + - $ref: "#/components/parameters/expand" + responses: + 200: + $ref: "#/components/responses/Project" + 404: + description: No project found + patch: + summary: updates the project context or sourceEnvironmentKey + parameters: + - $ref: "#/components/parameters/projectKey" + - $ref: "#/components/parameters/expand" + requestBody: + content: + application/json: + schema: + type: object + properties: + sourceEnvironmentKey: + type: string + description: environment to copy flag values from + context: + $ref: "#/components/schemas/Context" responses: 200: $ref: "#/components/responses/Project" 404: description: No project found - delete: summary: remove the specified project from the dev server parameters: @@ -75,15 +79,7 @@ paths: summary: Add the project to the dev server parameters: - $ref: "#/components/parameters/projectKey" - - name: expand - description: Available expand options for this endpoint. - in: query - schema: - type: array - items: - type: string - enum: - - overrides + - $ref: "#/components/parameters/expand" requestBody: content: application/json: @@ -95,13 +91,8 @@ paths: sourceEnvironmentKey: type: string description: environment to copy flag values from - # WIP be able to customize the context used - # context: - # type: object - # description: context object to use when evaluating flags in source environment - # default: - # key: "local-dev" - # kind: user + context: + $ref: "#/components/schemas/Context" responses: 201: $ref: "#/components/responses/Project" @@ -165,6 +156,16 @@ components: required: true schema: type: string + expand: + name: expand + description: Available expand options for this endpoint. + in: query + schema: + type: array + items: + type: string + enum: + - overrides schemas: FlagValue: description: value of a feature flag variation @@ -176,6 +177,15 @@ components: x-go-type: ldvalue.Value x-go-type-import: path: github.com/launchdarkly/go-sdk-common/v3/ldvalue + Context: + type: object + description: context object to use when evaluating flags in source environment + x-go-type: ldcontext.Context + x-go-type-import: + path: github.com/launchdarkly/go-sdk-common/v3/ldcontext + default: + key: "dev-environment" + kind: "user" Project: description: Project type: object @@ -184,18 +194,11 @@ components: - context - _lastSyncedFromSource properties: + context: + $ref: "#/components/schemas/Context" sourceEnvironmentKey: type: string description: environment to copy flag values from - context: - type: object - description: context object to use when evaluating flags in source environment - default: - key: "local-dev" - kind: user - x-go-type: ldcontext.Context - x-go-type-import: - path: github.com/launchdarkly/go-sdk-common/v3/ldcontext flagsState: type: object description: flags and their values and version for a given project in the source environment diff --git a/internal/dev_server/api/server.gen.go b/internal/dev_server/api/server.gen.go index 2eddf415..bf6c49b9 100644 --- a/internal/dev_server/api/server.gen.go +++ b/internal/dev_server/api/server.gen.go @@ -22,6 +22,11 @@ const ( GetDevProjectsProjectKeyParamsExpandOverrides GetDevProjectsProjectKeyParamsExpand = "overrides" ) +// Defines values for PatchDevProjectsProjectKeyParamsExpand. +const ( + PatchDevProjectsProjectKeyParamsExpandOverrides PatchDevProjectsProjectKeyParamsExpand = "overrides" +) + // Defines values for PostDevProjectsProjectKeyParamsExpand. const ( PostDevProjectsProjectKeyParamsExpandOverrides PostDevProjectsProjectKeyParamsExpand = "overrides" @@ -29,9 +34,12 @@ const ( // Defines values for PatchDevProjectsProjectKeySyncParamsExpand. const ( - Overrides PatchDevProjectsProjectKeySyncParamsExpand = "overrides" + PatchDevProjectsProjectKeySyncParamsExpandOverrides PatchDevProjectsProjectKeySyncParamsExpand = "overrides" ) +// Context context object to use when evaluating flags in source environment +type Context = ldcontext.Context + // FlagValue value of a feature flag variation type FlagValue = ldvalue.Value @@ -41,7 +49,7 @@ type Project struct { LastSyncedFromSource int64 `json:"_lastSyncedFromSource"` // Context context object to use when evaluating flags in source environment - Context ldcontext.Context `json:"context"` + Context Context `json:"context"` // FlagsState flags and their values and version for a given project in the source environment FlagsState *model.FlagsState `json:"flagsState,omitempty"` @@ -53,6 +61,9 @@ type Project struct { SourceEnvironmentKey string `json:"sourceEnvironmentKey"` } +// Expand defines model for expand. +type Expand = []string + // FlagKey defines model for flagKey. type FlagKey = string @@ -71,14 +82,35 @@ type FlagOverride struct { // GetDevProjectsProjectKeyParams defines parameters for GetDevProjectsProjectKey. type GetDevProjectsProjectKeyParams struct { // Expand Available expand options for this endpoint. - Expand *[]GetDevProjectsProjectKeyParamsExpand `form:"expand,omitempty" json:"expand,omitempty"` + Expand *Expand `form:"expand,omitempty" json:"expand,omitempty"` } // GetDevProjectsProjectKeyParamsExpand defines parameters for GetDevProjectsProjectKey. type GetDevProjectsProjectKeyParamsExpand string +// PatchDevProjectsProjectKeyJSONBody defines parameters for PatchDevProjectsProjectKey. +type PatchDevProjectsProjectKeyJSONBody struct { + // Context context object to use when evaluating flags in source environment + Context *Context `json:"context,omitempty"` + + // SourceEnvironmentKey environment to copy flag values from + SourceEnvironmentKey *string `json:"sourceEnvironmentKey,omitempty"` +} + +// PatchDevProjectsProjectKeyParams defines parameters for PatchDevProjectsProjectKey. +type PatchDevProjectsProjectKeyParams struct { + // Expand Available expand options for this endpoint. + Expand *Expand `form:"expand,omitempty" json:"expand,omitempty"` +} + +// PatchDevProjectsProjectKeyParamsExpand defines parameters for PatchDevProjectsProjectKey. +type PatchDevProjectsProjectKeyParamsExpand string + // PostDevProjectsProjectKeyJSONBody defines parameters for PostDevProjectsProjectKey. type PostDevProjectsProjectKeyJSONBody struct { + // Context context object to use when evaluating flags in source environment + Context *Context `json:"context,omitempty"` + // SourceEnvironmentKey environment to copy flag values from SourceEnvironmentKey string `json:"sourceEnvironmentKey"` } @@ -86,7 +118,7 @@ type PostDevProjectsProjectKeyJSONBody struct { // PostDevProjectsProjectKeyParams defines parameters for PostDevProjectsProjectKey. type PostDevProjectsProjectKeyParams struct { // Expand Available expand options for this endpoint. - Expand *[]PostDevProjectsProjectKeyParamsExpand `form:"expand,omitempty" json:"expand,omitempty"` + Expand *Expand `form:"expand,omitempty" json:"expand,omitempty"` } // PostDevProjectsProjectKeyParamsExpand defines parameters for PostDevProjectsProjectKey. @@ -95,12 +127,15 @@ type PostDevProjectsProjectKeyParamsExpand string // PatchDevProjectsProjectKeySyncParams defines parameters for PatchDevProjectsProjectKeySync. type PatchDevProjectsProjectKeySyncParams struct { // Expand Available expand options for this endpoint. - Expand *[]PatchDevProjectsProjectKeySyncParamsExpand `form:"expand,omitempty" json:"expand,omitempty"` + Expand *Expand `form:"expand,omitempty" json:"expand,omitempty"` } // PatchDevProjectsProjectKeySyncParamsExpand defines parameters for PatchDevProjectsProjectKeySync. type PatchDevProjectsProjectKeySyncParamsExpand string +// PatchDevProjectsProjectKeyJSONRequestBody defines body for PatchDevProjectsProjectKey for application/json ContentType. +type PatchDevProjectsProjectKeyJSONRequestBody PatchDevProjectsProjectKeyJSONBody + // PostDevProjectsProjectKeyJSONRequestBody defines body for PostDevProjectsProjectKey for application/json ContentType. type PostDevProjectsProjectKeyJSONRequestBody PostDevProjectsProjectKeyJSONBody @@ -118,6 +153,9 @@ type ServerInterface interface { // get the specified project and its configuration for syncing from the LaunchDarkly Service // (GET /dev/projects/{projectKey}) GetDevProjectsProjectKey(w http.ResponseWriter, r *http.Request, projectKey ProjectKey, params GetDevProjectsProjectKeyParams) + // updates the project context or sourceEnvironmentKey + // (PATCH /dev/projects/{projectKey}) + PatchDevProjectsProjectKey(w http.ResponseWriter, r *http.Request, projectKey ProjectKey, params PatchDevProjectsProjectKeyParams) // Add the project to the dev server // (POST /dev/projects/{projectKey}) PostDevProjectsProjectKey(w http.ResponseWriter, r *http.Request, projectKey ProjectKey, params PostDevProjectsProjectKeyParams) @@ -219,6 +257,43 @@ func (siw *ServerInterfaceWrapper) GetDevProjectsProjectKey(w http.ResponseWrite handler.ServeHTTP(w, r.WithContext(ctx)) } +// PatchDevProjectsProjectKey operation middleware +func (siw *ServerInterfaceWrapper) PatchDevProjectsProjectKey(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + var err error + + // ------------- Path parameter "projectKey" ------------- + var projectKey ProjectKey + + err = runtime.BindStyledParameterWithOptions("simple", "projectKey", mux.Vars(r)["projectKey"], &projectKey, runtime.BindStyledParameterOptions{Explode: false, Required: true}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "projectKey", Err: err}) + return + } + + // Parameter object where we will unmarshal all parameters from the context + var params PatchDevProjectsProjectKeyParams + + // ------------- Optional query parameter "expand" ------------- + + err = runtime.BindQueryParameter("form", true, false, "expand", r.URL.Query(), ¶ms.Expand) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "expand", Err: err}) + return + } + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.PatchDevProjectsProjectKey(w, r, projectKey, params) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r.WithContext(ctx)) +} + // PostDevProjectsProjectKey operation middleware func (siw *ServerInterfaceWrapper) PostDevProjectsProjectKey(w http.ResponseWriter, r *http.Request) { ctx := r.Context() @@ -482,6 +557,8 @@ func HandlerWithOptions(si ServerInterface, options GorillaServerOptions) http.H r.HandleFunc(options.BaseURL+"/dev/projects/{projectKey}", wrapper.GetDevProjectsProjectKey).Methods("GET") + r.HandleFunc(options.BaseURL+"/dev/projects/{projectKey}", wrapper.PatchDevProjectsProjectKey).Methods("PATCH") + r.HandleFunc(options.BaseURL+"/dev/projects/{projectKey}", wrapper.PostDevProjectsProjectKey).Methods("POST") r.HandleFunc(options.BaseURL+"/dev/projects/{projectKey}/overrides/{flagKey}", wrapper.DeleteDevProjectsProjectKeyOverridesFlagKey).Methods("DELETE") @@ -569,6 +646,33 @@ func (response GetDevProjectsProjectKey404Response) VisitGetDevProjectsProjectKe return nil } +type PatchDevProjectsProjectKeyRequestObject struct { + ProjectKey ProjectKey `json:"projectKey"` + Params PatchDevProjectsProjectKeyParams + Body *PatchDevProjectsProjectKeyJSONRequestBody +} + +type PatchDevProjectsProjectKeyResponseObject interface { + VisitPatchDevProjectsProjectKeyResponse(w http.ResponseWriter) error +} + +type PatchDevProjectsProjectKey200JSONResponse struct{ ProjectJSONResponse } + +func (response PatchDevProjectsProjectKey200JSONResponse) VisitPatchDevProjectsProjectKeyResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type PatchDevProjectsProjectKey404Response struct { +} + +func (response PatchDevProjectsProjectKey404Response) VisitPatchDevProjectsProjectKeyResponse(w http.ResponseWriter) error { + w.WriteHeader(404) + return nil +} + type PostDevProjectsProjectKeyRequestObject struct { ProjectKey ProjectKey `json:"projectKey"` Params PostDevProjectsProjectKeyParams @@ -669,6 +773,9 @@ type StrictServerInterface interface { // get the specified project and its configuration for syncing from the LaunchDarkly Service // (GET /dev/projects/{projectKey}) GetDevProjectsProjectKey(ctx context.Context, request GetDevProjectsProjectKeyRequestObject) (GetDevProjectsProjectKeyResponseObject, error) + // updates the project context or sourceEnvironmentKey + // (PATCH /dev/projects/{projectKey}) + PatchDevProjectsProjectKey(ctx context.Context, request PatchDevProjectsProjectKeyRequestObject) (PatchDevProjectsProjectKeyResponseObject, error) // Add the project to the dev server // (POST /dev/projects/{projectKey}) PostDevProjectsProjectKey(ctx context.Context, request PostDevProjectsProjectKeyRequestObject) (PostDevProjectsProjectKeyResponseObject, error) @@ -789,6 +896,40 @@ func (sh *strictHandler) GetDevProjectsProjectKey(w http.ResponseWriter, r *http } } +// PatchDevProjectsProjectKey operation middleware +func (sh *strictHandler) PatchDevProjectsProjectKey(w http.ResponseWriter, r *http.Request, projectKey ProjectKey, params PatchDevProjectsProjectKeyParams) { + var request PatchDevProjectsProjectKeyRequestObject + + request.ProjectKey = projectKey + request.Params = params + + var body PatchDevProjectsProjectKeyJSONRequestBody + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't decode JSON body: %w", err)) + return + } + request.Body = &body + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.PatchDevProjectsProjectKey(ctx, request.(PatchDevProjectsProjectKeyRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "PatchDevProjectsProjectKey") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(PatchDevProjectsProjectKeyResponseObject); ok { + if err := validResponse.VisitPatchDevProjectsProjectKeyResponse(w); err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } + } else if response != nil { + sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response)) + } +} + // PostDevProjectsProjectKey operation middleware func (sh *strictHandler) PostDevProjectsProjectKey(w http.ResponseWriter, r *http.Request, projectKey ProjectKey, params PostDevProjectsProjectKeyParams) { var request PostDevProjectsProjectKeyRequestObject diff --git a/internal/dev_server/api/server.go b/internal/dev_server/api/server.go index 02cc918f..f5c5dbe9 100644 --- a/internal/dev_server/api/server.go +++ b/internal/dev_server/api/server.go @@ -87,7 +87,7 @@ func (s Server) GetDevProjectsProjectKey(ctx context.Context, request GetDevProj func (s Server) PostDevProjectsProjectKey(ctx context.Context, request PostDevProjectsProjectKeyRequestObject) (PostDevProjectsProjectKeyResponseObject, error) { store := model.StoreFromContext(ctx) - project, err := model.CreateProject(ctx, request.ProjectKey, request.Body.SourceEnvironmentKey, nil) + project, err := model.CreateProject(ctx, request.ProjectKey, request.Body.SourceEnvironmentKey, request.Body.Context) if err != nil { return nil, err } @@ -127,6 +127,41 @@ func (s Server) PostDevProjectsProjectKey(ctx context.Context, request PostDevPr }, nil } +func (s Server) PatchDevProjectsProjectKey(ctx context.Context, request PatchDevProjectsProjectKeyRequestObject) (PatchDevProjectsProjectKeyResponseObject, error) { + store := model.StoreFromContext(ctx) + project, err := model.UpdateProject(ctx, request.ProjectKey, request.Body.Context, request.Body.SourceEnvironmentKey) + if err != nil { + return nil, err + } + if project.Key == "" && project.SourceEnvironmentKey == "" { + return PatchDevProjectsProjectKey404Response{}, nil + } + + response := ProjectJSONResponse{ + LastSyncedFromSource: project.LastSyncTime.Unix(), + Context: project.Context, + SourceEnvironmentKey: project.SourceEnvironmentKey, + FlagsState: &project.FlagState, + } + + if request.Params.Expand != nil { + for _, item := range *request.Params.Expand { + if item == "overrides" { + overrides, err := store.GetOverridesForProject(ctx, request.ProjectKey) + if err != nil { + return nil, err + } + response.Overrides = &overrides + } + } + + } + + return PatchDevProjectsProjectKey200JSONResponse{ + response, + }, nil +} + func (s Server) PatchDevProjectsProjectKeySync(ctx context.Context, request PatchDevProjectsProjectKeySyncRequestObject) (PatchDevProjectsProjectKeySyncResponseObject, error) { store := model.StoreFromContext(ctx) project, err := model.SyncProject(ctx, request.ProjectKey) diff --git a/internal/dev_server/db/sqlite.go b/internal/dev_server/db/sqlite.go index f4423d7e..0d3f416a 100644 --- a/internal/dev_server/db/sqlite.go +++ b/internal/dev_server/db/sqlite.go @@ -74,9 +74,9 @@ func (s Sqlite) UpdateProject(ctx context.Context, project model.Project) (bool, result, err := s.database.ExecContext(ctx, ` UPDATE projects - SET flag_state = ?, last_sync_time = ? + SET flag_state = ?, last_sync_time = ?, context=? WHERE key = ?; - `, flagsStateJson, project.LastSyncTime, project.Key) + `, flagsStateJson, project.LastSyncTime, project.Context.JSONString(), project.Key) rowsAffected, err := result.RowsAffected() if err != nil { diff --git a/internal/dev_server/model/project.go b/internal/dev_server/model/project.go index c6963bb2..9562cbb9 100644 --- a/internal/dev_server/model/project.go +++ b/internal/dev_server/model/project.go @@ -43,6 +43,39 @@ func CreateProject(ctx context.Context, projectKey, sourceEnvironmentKey string, return project, nil } +func UpdateProject(ctx context.Context, projectKey string, context *ldcontext.Context, sourceEnvironmentKey *string) (Project, error) { + store := StoreFromContext(ctx) + project, err := store.GetDevProject(ctx, projectKey) + if err != nil { + return Project{}, err + } + if context != nil { + project.Context = *context + } + + if sourceEnvironmentKey != nil { + project.SourceEnvironmentKey = *sourceEnvironmentKey + } + + if context != nil || sourceEnvironmentKey != nil { + flagsState, err := project.FetchFlagState(ctx) + if err != nil { + return Project{}, err + } + project.FlagState = flagsState + project.LastSyncTime = time.Now() + } + + updated, err := store.UpdateProject(ctx, *project) + if err != nil { + return Project{}, err + } + if !updated { + return Project{}, err + } + return *project, nil +} + func SyncProject(ctx context.Context, projectKey string) (Project, error) { store := StoreFromContext(ctx) project, err := store.GetDevProject(ctx, projectKey) From e2cb81189e0ff04a4a234fab1ab0865f9bf297df Mon Sep 17 00:00:00 2001 From: Hosh Date: Fri, 28 Jun 2024 20:55:52 +0100 Subject: [PATCH 48/79] fix: fix override (#354) bug: fix override --- internal/dev_server/api/server.go | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/internal/dev_server/api/server.go b/internal/dev_server/api/server.go index f5c5dbe9..ed133272 100644 --- a/internal/dev_server/api/server.go +++ b/internal/dev_server/api/server.go @@ -151,7 +151,17 @@ func (s Server) PatchDevProjectsProjectKey(ctx context.Context, request PatchDev if err != nil { return nil, err } - response.Overrides = &overrides + respOverrides := make(model.FlagsState) + for _, override := range overrides { + if !override.Active { + continue + } + respOverrides[override.FlagKey] = model.FlagState{ + Value: override.Value, + Version: override.Version, + } + } + response.Overrides = &respOverrides } } From 4e0cdbac3ae01d1eff480c3edc76b8cc127533a0 Mon Sep 17 00:00:00 2001 From: Kelly Hofmann <55991524+k3llymariee@users.noreply.github.com> Date: Fri, 28 Jun 2024 13:16:40 -0700 Subject: [PATCH 49/79] add UI cmd (#355) add new dev-server ui cmd --- cmd/dev_server/dev_server.go | 1 + cmd/dev_server/start_server.go | 42 +++++++++++++++++++++++++++++++ internal/dev_server/dev_server.go | 1 + 3 files changed, 44 insertions(+) diff --git a/cmd/dev_server/dev_server.go b/cmd/dev_server/dev_server.go index a1931a09..58316457 100644 --- a/cmd/dev_server/dev_server.go +++ b/cmd/dev_server/dev_server.go @@ -37,6 +37,7 @@ func NewDevServerCmd(localClient dev_server.LocalClient, ldClient dev_server.Cli cmd.AddGroup(&cobra.Group{ID: "server", Title: "Server commands:"}) cmd.AddCommand(NewStartServerCmd(ldClient)) + cmd.AddCommand(NewUICmd()) cmd.SetUsageTemplate(resourcecmd.SubcommandUsageTemplate()) diff --git a/cmd/dev_server/start_server.go b/cmd/dev_server/start_server.go index 4dd13339..3ca9d3ee 100644 --- a/cmd/dev_server/start_server.go +++ b/cmd/dev_server/start_server.go @@ -2,6 +2,10 @@ package dev_server import ( "context" + "fmt" + "log" + "os/exec" + "runtime" "github.com/spf13/cobra" "github.com/spf13/viper" @@ -41,3 +45,41 @@ func startServer(client dev_server.Client) func(*cobra.Command, []string) error return nil } } + +func NewUICmd() *cobra.Command { + cmd := &cobra.Command{ + GroupID: "server", + Args: validators.Validate(), + Long: "open the dev ui in your default browser", + RunE: openUI(), + Short: "open the ui", + Use: "ui", + } + + cmd.SetUsageTemplate(resourcescmd.SubcommandUsageTemplate()) + + return cmd +} + +func openUI() func(*cobra.Command, []string) error { + return func(cmd *cobra.Command, args []string) error { + url := "http://localhost:8765/ui" + + var err error + switch runtime.GOOS { + case "linux": + err = exec.Command("xdg-open", url).Start() + case "windows": + err = exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start() + case "darwin": + err = exec.Command("open", url).Start() + default: + err = fmt.Errorf("unsupported platform") + } + if err != nil { + log.Fatal(err) + } + + return nil + } +} diff --git a/internal/dev_server/dev_server.go b/internal/dev_server/dev_server.go index 88fc7bf8..844a5f67 100644 --- a/internal/dev_server/dev_server.go +++ b/internal/dev_server/dev_server.go @@ -57,6 +57,7 @@ func (c LDClient) RunServer(ctx context.Context, accessToken, baseURI, devStream handler = handlers.CombinedLoggingHandler(os.Stdout, handler) handler = handlers.RecoveryHandler(handlers.PrintRecoveryStack(true))(handler) fmt.Println("Server running on 0.0.0.0:8765") + fmt.Println("Access the UI for toggling overrides at http://localhost:8765/ui or by running `ldcli dev-server ui`") server := http.Server{ Addr: "0.0.0.0:8765", Handler: handler, From 99c82f512d4c2bef52b805d5f7026233b530ec79 Mon Sep 17 00:00:00 2001 From: Mike Zorn Date: Fri, 28 Jun 2024 15:59:35 -0700 Subject: [PATCH 50/79] feat: add mobile & PHP support (#356) This adds support for mobile and PHP SDKs. It also relocates the list of the supported endpoints to the package docs instead of a random comment. --- internal/dev_server/adapters/sdk.go | 4 +- internal/dev_server/sdk/cors.go | 13 ++++ internal/dev_server/sdk/docs.go | 34 ++++++++- internal/dev_server/sdk/get_server_flags.go | 39 ++++++++++ .../dev_server/sdk/project_key_middleware.go | 10 ++- internal/dev_server/sdk/routes.go | 73 +++++++------------ internal/dev_server/sdk/server_flags.go | 4 +- .../dev_server/sdk/stream_server_flags.go | 2 +- 8 files changed, 125 insertions(+), 54 deletions(-) create mode 100644 internal/dev_server/sdk/get_server_flags.go diff --git a/internal/dev_server/adapters/sdk.go b/internal/dev_server/adapters/sdk.go index 07a6f7d3..bdca9e7e 100644 --- a/internal/dev_server/adapters/sdk.go +++ b/internal/dev_server/adapters/sdk.go @@ -45,7 +45,9 @@ func (s Sdk) GetAllFlagsState(ctx context.Context, ldContext ldcontext.Context, } defer func() { err := ldClient.Close() - log.Printf("error while closing SDK client: %+v", err) + if err != nil { + log.Printf("error while closing SDK client: %+v", err) + } }() flags := ldClient.AllFlagsState(ldContext) return flags, nil diff --git a/internal/dev_server/sdk/cors.go b/internal/dev_server/sdk/cors.go index 0ee7a24c..33a7aeb7 100644 --- a/internal/dev_server/sdk/cors.go +++ b/internal/dev_server/sdk/cors.go @@ -7,6 +7,19 @@ func CorsHeaders(handler http.Handler) http.Handler { writer.Header().Set("Access-Control-Allow-Origin", "*") writer.Header().Set("Access-Control-Allow-Methods", "GET,OPTIONS") writer.Header().Set("Access-Control-Allow-Headers", "Cache-Control,Content-Type,Content-Length,Accept-Encoding,X-LaunchDarkly-User-Agent,X-LaunchDarkly-Payload-ID,X-LaunchDarkly-Wrapper,X-LaunchDarkly-Event-Schema,X-LaunchDarkly-Tags") + writer.Header().Set("Access-Control-Expose-Headers:", "Date") + writer.Header().Set("Access-Control-Max-Age", "300") + handler.ServeHTTP(writer, request) + }) +} + +func EventsCorsHeaders(handler http.Handler) http.Handler { + return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { + writer.Header().Set("Access-Control-Allow-Origin", "*") + writer.Header().Set("Access-Control-Allow-Methods", "POST,OPTIONS") + writer.Header().Set("Access-Control-Allow-Headers", "Accept,Content-Type,Content-Length,Accept-Encoding,X-LaunchDarkly-Event-Schema,X-LaunchDarkly-User-Agent,X-LaunchDarkly-Payload-ID,X-LaunchDarkly-Wrapper,X-LaunchDarkly-Tags") + writer.Header().Set("Access-Control-Expose-Headers:", "Date") + writer.Header().Set("Access-Control-Max-Age", "300") handler.ServeHTTP(writer, request) }) } diff --git a/internal/dev_server/sdk/docs.go b/internal/dev_server/sdk/docs.go index 51514dce..915adc6c 100644 --- a/internal/dev_server/sdk/docs.go +++ b/internal/dev_server/sdk/docs.go @@ -1,2 +1,34 @@ -// Package sdk provides API handlers for [all the endpoints provided by ld-relay](https://github.com/launchdarkly/ld-relay/blob/v8/docs/endpoints.md). It also will register observers with the data model to support streaming. +// Package sdk provides API handlers for [the endpoints provided by ld-relay](https://github.com/launchdarkly/ld-relay/blob/v8/docs/endpoints.md). +// +// Not all routes are supported because some routes are for exclusively for SDKs that have reached end of life. +// +// ✅ /all GET stream. SSE stream for all data +// ✅ /bulk POST events. Receives analytics events from SDKs +// ✅ /diagnostic POST events. Receives diagnostic data from SDKs +// ❌ /flags GET stream. SSE stream for flag data (older SDKs) +// ✅ /sdk/flags GET sdk. Polling endpoint for PHP SDK +// ✅ /sdk/flags/{flagKey} GET sdk. Polling endpoint for PHP SDK +// ✅ /sdk/segments/{segmentKey} GET sdk. Polling endpoint for PHP SDK (Segments are not used in dev server, so this always 404s) +// ✅ /meval/{contextBase64} GET clientstream. SSE stream of "ping" and other events +// ✅ /meval REPORT clientstream. Same as above, but request body is the evaluation context JSON object (not in base64) +// ✅ /mobile POST events. For receiving events from mobile SDKs +// ✅ /mobile/events POST events. Same as above +// ✅ /mobile/events/bulk POST events. Same as above +// ✅ /mobile/events/diagnostic POST events. Same as above +// ❌ /mping GET clientstream. SSE stream for older SDKs that issues "ping" events when flags have changed +// ✅ /msdk/evalx/contexts/{contextBase64} GET clientsdk. Polling endpoint, returns flag evaluation results for an evaluation context +// ✅ /msdk/evalx/context REPORT clientsdk. Same as above but request body is the evaluation context JSON object (not in base64) +// ✅ /msdk/evalx/users/{contextBase64} GET clientsdk. Alternate name for /msdk/evalx/contexts/{contextBase64} used by older SDKs +// ✅ /msdk/evalx/user REPORT clientsdk. Alternate name for /msdk/evalx/context used by older SDKs +// ❌ /a/{envId}.gif?d=*events* GET events. Alternative analytics event mechanism used if browser does not allow CORS +// ✅ /eval/{envId}/{contextBase64} GET clientstream. SSE stream of "ping" and other events for JS and other client-side SDK listeners +// ✅ /eval/{envId} REPORT clientstream. Same as above but request body is the evaluation context JSON object (not in base64) +// ✅ /events/bulk/{envId} POST events. Receives analytics events from SDKs +// ✅ /events/diagnostic/{envId} POST events. Receives diagnostic data from SDKs +// ❌ /ping/{envId} GET clientstream. SSE stream for older SDKs that issues "ping" events when flags have changed +// ✅ /sdk/evalx/{envId}/contexts/{contextBase64} GET clientsdk. Polling endpoint, returns flag evaluation results and additional metadata +// ✅ /sdk/evalx/{envId}/contexts REPORT clientsdk. Same as above but request body is the evaluation context JSON object (not in base64) +// ✅ /sdk/evalx/{envId}/users/{contextBase64} GET clientsdk. Alternate name for /sdk/evalx/{envId}/contexts/{contextBase64} used by older SDKs +// ✅ /sdk/evalx/{envId}/users REPORT clientsdk. Alternate name for /sdk/evalx/{envId}/contexts used by older SDKs +// ✅ /sdk/goals/{envId} GET clientsdk. Provides goals data used by JS SDK package sdk diff --git a/internal/dev_server/sdk/get_server_flags.go b/internal/dev_server/sdk/get_server_flags.go new file mode 100644 index 00000000..ce2749c9 --- /dev/null +++ b/internal/dev_server/sdk/get_server_flags.go @@ -0,0 +1,39 @@ +package sdk + +import ( + "encoding/json" + "net/http" + + "github.com/gorilla/mux" + "github.com/launchdarkly/ldcli/internal/dev_server/model" + "github.com/pkg/errors" +) + +func GetServerFlags(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + store := model.StoreFromContext(ctx) + projectKey := GetProjectKeyFromContext(ctx) + project, err := store.GetDevProject(ctx, projectKey) + if err != nil { + panic(errors.Wrap(err, "unable to get dev project")) + } + allFlags, err := project.GetFlagStateWithOverridesForProject(ctx) + if err != nil { + panic(errors.Wrap(err, "failed to get flag state")) + } + var body interface{} + if flagKey, ok := mux.Vars(r)["flagKey"]; ok { + body = ServerFlagsFromFlagsState(allFlags)[flagKey] + } else { + body = ServerAllPayloadFromFlagsState(allFlags) + } + jsonBody, err := json.Marshal(body) + if err != nil { + panic(errors.Wrap(err, "failed to marshal flag state")) + } + w.Header().Set("Content-Type", "application/json") + _, err = w.Write(jsonBody) + if err != nil { + panic(errors.Wrap(err, "unable to write response")) + } +} diff --git a/internal/dev_server/sdk/project_key_middleware.go b/internal/dev_server/sdk/project_key_middleware.go index bb574236..8fdb5675 100644 --- a/internal/dev_server/sdk/project_key_middleware.go +++ b/internal/dev_server/sdk/project_key_middleware.go @@ -21,7 +21,11 @@ func GetProjectKeyFromContext(ctx context.Context) string { func GetProjectKeyFromEnvIdParameter(pathParameter string) func(handler http.Handler) http.Handler { return func(handler http.Handler) http.Handler { return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { - projectKey := mux.Vars(request)[pathParameter] + projectKey, ok := mux.Vars(request)[pathParameter] + if !ok { + http.Error(writer, "project key not on path", http.StatusNotFound) + return + } ctx := request.Context() ctx = SetProjectKeyOnContext(ctx, projectKey) request = request.WithContext(ctx) @@ -34,6 +38,10 @@ func GetProjectKeyFromAuthorizationHeader(handler http.Handler) http.Handler { return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { ctx := request.Context() projectKey := request.Header.Get("Authorization") + if projectKey == "" { + http.Error(writer, "project key not on Authorization header", http.StatusUnauthorized) + return + } ctx = SetProjectKeyOnContext(ctx, projectKey) request = request.WithContext(ctx) handler.ServeHTTP(writer, request) diff --git a/internal/dev_server/sdk/routes.go b/internal/dev_server/sdk/routes.go index 6a2a8e82..29bc1bdb 100644 --- a/internal/dev_server/sdk/routes.go +++ b/internal/dev_server/sdk/routes.go @@ -12,9 +12,8 @@ func BindRoutes(router *mux.Router) { // events router.HandleFunc("/bulk", DevNull) router.HandleFunc("/diagnostic", DevNull) - // TODO need cors for events on client side - router.HandleFunc("/events/bulk/{envId}", DevNull) - router.HandleFunc("/events/diagnostic/{envId}", DevNull) + router.Handle("/events/bulk/{envId}", EventsCorsHeaders(DevNull)) + router.Handle("/events/diagnostic/{envId}", EventsCorsHeaders(DevNull)) router.HandleFunc("/mobile", DevNull) router.HandleFunc("/mobile/events", DevNull) router.HandleFunc("/mobile/events/bulk", DevNull) @@ -22,52 +21,30 @@ func BindRoutes(router *mux.Router) { router.Handle("/all", GetProjectKeyFromAuthorizationHeader(http.HandlerFunc(StreamServerAllPayload))) + router.PathPrefix("/sdk/flags"). + Methods(http.MethodGet). + Handler(GetProjectKeyFromAuthorizationHeader(http.HandlerFunc(GetServerFlags))) + + router.PathPrefix("/meval").Handler(GetProjectKeyFromAuthorizationHeader(http.HandlerFunc(StreamClientFlags))) + router.PathPrefix("/msdk/evalx").Handler(GetProjectKeyFromAuthorizationHeader(http.HandlerFunc(GetClientFlags))) + evalRouter := router.PathPrefix("/eval").Subrouter() evalRouter.Use(CorsHeaders) - evalRouter.Methods("OPTIONS").HandlerFunc(ConstantResponseHandler(http.StatusOK, "")) + evalRouter.Methods(http.MethodOptions).HandlerFunc(ConstantResponseHandler(http.StatusOK, "")) evalRouter.Use(GetProjectKeyFromEnvIdParameter("envId")) - evalRouter.HandleFunc("/{envId}", StreamClientFlags) - evalRouter.HandleFunc("/{envId}/{contextBase64}", StreamClientFlags) - - clientsideSdkRouter := router.PathPrefix("/sdk").Subrouter() - clientsideSdkRouter.Use(CorsHeaders) - clientsideSdkRouter.Methods("OPTIONS").HandlerFunc(ConstantResponseHandler(http.StatusOK, "")) - clientsideSdkRouter.Use(GetProjectKeyFromEnvIdParameter("envId")) - clientsideSdkRouter.HandleFunc("/goals/{envId}", ConstantResponseHandler(http.StatusOK, "[]")) - clientsideSdkRouter.HandleFunc("/evalx/{envId}/contexts/{contextBase64}", GetClientFlags) - clientsideSdkRouter.HandleFunc("/evalx/{envId}/contexts", GetClientFlags) - clientsideSdkRouter.HandleFunc("/evalx/{envId}/users", GetClientFlags) - clientsideSdkRouter.HandleFunc("/evalx/{envId}/users/{userBase64}", GetClientFlags) - - /* - ✅ /all GET stream. SSE stream for all data - ✅ /bulk POST events. Receives analytics events from SDKs - ✅ /diagnostic POST events. Receives diagnostic data from SDKs - /flags GET stream. SSE stream for flag data (older SDKs) - /sdk/flags GET sdk. Polling endpoint for PHP SDK - /sdk/flags/{flagKey} GET sdk. Polling endpoint for PHP SDK - /sdk/segments/{segmentKey} GET sdk. Polling endpoint for PHP SDK - /meval/{contextBase64} GET clientstream. SSE stream of "ping" and other events - /meval REPORT clientstream. Same as above, but request body is the evaluation context JSON object (not in base64) - ✅ /mobile POST events. For receiving events from mobile SDKs - ✅ /mobile/events POST events. Same as above - ✅ /mobile/events/bulk POST events. Same as above - ✅ /mobile/events/diagnostic POST events. Same as above - /mping GET clientstream. SSE stream for older SDKs that issues "ping" events when flags have changed - /msdk/evalx/contexts/{contextBase64} GET clientsdk. Polling endpoint, returns flag evaluation results for an evaluation context - /msdk/evalx/context REPORT clientsdk. Same as above but request body is the evaluation context JSON object (not in base64) - /msdk/evalx/users/{contextBase64} GET clientsdk. Alternate name for /msdk/evalx/contexts/{contextBase64} used by older SDKs - /msdk/evalx/user REPORT clientsdk. Alternate name for /msdk/evalx/context used by older SDKs - /a/{envId}.gif?d=*events* GET events. Alternative analytics event mechanism used if browser does not allow CORS - ✅ /eval/{envId}/{contextBase64} GET clientstream. SSE stream of "ping" and other events for JS and other client-side SDK listeners - ✅ /eval/{envId} REPORT clientstream. Same as above but request body is the evaluation context JSON object (not in base64) - ✅ /events/bulk/{envId} POST events. Receives analytics events from SDKs - ✅ /events/diagnostic/{envId} POST events. Receives diagnostic data from SDKs - /ping/{envId} GET clientstream. SSE stream for older SDKs that issues "ping" events when flags have changed - ✅ /sdk/evalx/{envId}/contexts/{contextBase64} GET clientsdk. Polling endpoint, returns flag evaluation results and additional metadata - ✅ /sdk/evalx/{envId}/contexts REPORT clientsdk. Same as above but request body is the evaluation context JSON object (not in base64) - ✅ /sdk/evalx/{envId}/users/{contextBase64} GET clientsdk. Alternate name for /sdk/evalx/{envId}/contexts/{contextBase64} used by older SDKs - ✅ /sdk/evalx/{envId}/users REPORT clientsdk. Alternate name for /sdk/evalx/{envId}/contexts used by older SDKs - ✅ /sdk/goals/{envId} GET clientsdk. Provides goals data used by JS SDK - */ + evalRouter.PathPrefix("/{envId}"). + Methods(http.MethodGet, "REPORT"). + HandlerFunc(StreamClientFlags) + + goalsRouter := router.Path("/sdk/goals/{envId}").Subrouter() + goalsRouter.Use(CorsHeaders) + goalsRouter.Use(GetProjectKeyFromEnvIdParameter("envId")) + goalsRouter.Methods(http.MethodOptions).HandlerFunc(ConstantResponseHandler(http.StatusOK, "")) + goalsRouter.Methods(http.MethodGet).HandlerFunc(ConstantResponseHandler(http.StatusOK, "[]")) + + evalXRouter := router.PathPrefix("/sdk/evalx/{envId}").Subrouter() + evalXRouter.Use(CorsHeaders) + evalXRouter.Use(GetProjectKeyFromEnvIdParameter("envId")) + evalXRouter.Methods(http.MethodOptions).HandlerFunc(ConstantResponseHandler(http.StatusOK, "")) + evalXRouter.Methods(http.MethodGet, "REPORT").HandlerFunc(GetClientFlags) } diff --git a/internal/dev_server/sdk/server_flags.go b/internal/dev_server/sdk/server_flags.go index c8e82672..e359acde 100644 --- a/internal/dev_server/sdk/server_flags.go +++ b/internal/dev_server/sdk/server_flags.go @@ -53,12 +53,12 @@ func ServerAllPayloadFromFlagsState(state model.FlagsState) ServerAllPayload { func ServerFlagsFromFlagsState(flagsState model.FlagsState) ServerFlags { serverFlags := make(map[string]ServerFlag, len(flagsState)) for flagKey, state := range flagsState { - serverFlags[flagKey] = ServerFlagFromFlagState(flagKey, state) + serverFlags[flagKey] = serverFlagFromFlagState(flagKey, state) } return serverFlags } -func ServerFlagFromFlagState(key string, state model.FlagState) ServerFlag { +func serverFlagFromFlagState(key string, state model.FlagState) ServerFlag { return ServerFlag{ Key: key, On: true, diff --git a/internal/dev_server/sdk/stream_server_flags.go b/internal/dev_server/sdk/stream_server_flags.go index 4ef2b1d9..6f70731b 100644 --- a/internal/dev_server/sdk/stream_server_flags.go +++ b/internal/dev_server/sdk/stream_server_flags.go @@ -58,7 +58,7 @@ func (c serverFlagsObserver) Handle(event interface{}) { } data, err := json.Marshal(serverSidePatchData{ Path: fmt.Sprintf("/flags/%s", event.FlagKey), - Data: ServerFlagFromFlagState(event.FlagKey, event.FlagState), + Data: serverFlagFromFlagState(event.FlagKey, event.FlagState), }) if err != nil { panic(errors.Wrap(err, "failed to marshal flag state in observer")) From 0eb9b4f4e5a960d7c544a4c81f149d02fa7d2715 Mon Sep 17 00:00:00 2001 From: Kelly Hofmann <55991524+k3llymariee@users.noreply.github.com> Date: Fri, 28 Jun 2024 17:14:45 -0700 Subject: [PATCH 51/79] fix error response expected by ldcli cmds (#357) --- internal/dev_server/api/api.yaml | 16 +++++++++++++++- internal/dev_server/api/server.gen.go | 20 ++++++++++++++++---- internal/dev_server/api/server.go | 5 ++++- 3 files changed, 35 insertions(+), 6 deletions(-) diff --git a/internal/dev_server/api/api.yaml b/internal/dev_server/api/api.yaml index 74d9228c..4b3ed202 100644 --- a/internal/dev_server/api/api.yaml +++ b/internal/dev_server/api/api.yaml @@ -74,7 +74,7 @@ paths: 204: description: OK. Project & overrides were removed 404: - description: no project found + $ref: "#/components/responses/NotFoundErrorResp" post: summary: Add the project to the dev server parameters: @@ -237,3 +237,17 @@ components: application/json: schema: $ref: "#/components/schemas/Project" + NotFoundErrorResp: + description: not found + content: + application/json: + schema: + type: object + required: + - code + - message + properties: + code: + type: string + message: + type: string diff --git a/internal/dev_server/api/server.gen.go b/internal/dev_server/api/server.gen.go index bf6c49b9..6afbb3e1 100644 --- a/internal/dev_server/api/server.gen.go +++ b/internal/dev_server/api/server.gen.go @@ -79,6 +79,12 @@ type FlagOverride struct { Value FlagValue `json:"value"` } +// NotFoundErrorResp defines model for NotFoundErrorResp. +type NotFoundErrorResp struct { + Code string `json:"code"` + Message string `json:"message"` +} + // GetDevProjectsProjectKeyParams defines parameters for GetDevProjectsProjectKey. type GetDevProjectsProjectKeyParams struct { // Expand Available expand options for this endpoint. @@ -578,6 +584,11 @@ type FlagOverrideJSONResponse struct { Value FlagValue `json:"value"` } +type NotFoundErrorRespJSONResponse struct { + Code string `json:"code"` + Message string `json:"message"` +} + type ProjectJSONResponse Project type GetDevProjectsRequestObject struct { @@ -612,12 +623,13 @@ func (response DeleteDevProjectsProjectKey204Response) VisitDeleteDevProjectsPro return nil } -type DeleteDevProjectsProjectKey404Response struct { -} +type DeleteDevProjectsProjectKey404JSONResponse struct{ NotFoundErrorRespJSONResponse } -func (response DeleteDevProjectsProjectKey404Response) VisitDeleteDevProjectsProjectKeyResponse(w http.ResponseWriter) error { +func (response DeleteDevProjectsProjectKey404JSONResponse) VisitDeleteDevProjectsProjectKeyResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") w.WriteHeader(404) - return nil + + return json.NewEncoder(w).Encode(response) } type GetDevProjectsProjectKeyRequestObject struct { diff --git a/internal/dev_server/api/server.go b/internal/dev_server/api/server.go index ed133272..20796bdd 100644 --- a/internal/dev_server/api/server.go +++ b/internal/dev_server/api/server.go @@ -35,7 +35,10 @@ func (s Server) DeleteDevProjectsProjectKey(ctx context.Context, request DeleteD return nil, err } if !deleted { - return DeleteDevProjectsProjectKey404Response{}, nil + return DeleteDevProjectsProjectKey404JSONResponse{NotFoundErrorRespJSONResponse{ + Code: "not_found", + Message: "project not found", + }}, nil } return DeleteDevProjectsProjectKey204Response{}, nil } From 93c4c6f2085b009ad2e8f0fa237ce643987d1f5b Mon Sep 17 00:00:00 2001 From: Mike Zorn Date: Mon, 1 Jul 2024 10:19:46 -0700 Subject: [PATCH 52/79] fix: error handling when project is missing (#360) Improves error handling when an sdk connects and a project is missing. Previous behavior was a scary panic log. Now there's a more helpful ``` 2024/06/28 16:26:02 project, default2, not found 2024/06/28 16:26:02 You may need to call ldcli dev-server add-project ``` --- internal/dev_server/adapters/sdk.go | 2 + internal/dev_server/api/server.go | 3 +- internal/dev_server/db/sqlite.go | 11 ++--- internal/dev_server/model/store.go | 4 ++ internal/dev_server/sdk/get_client_flags.go | 18 ++++---- internal/dev_server/sdk/get_server_flags.go | 23 +++++----- internal/dev_server/sdk/store_facade.go | 42 +++++++++++++++++++ .../dev_server/sdk/stream_client_flags.go | 18 ++++---- .../dev_server/sdk/stream_server_flags.go | 16 ++++--- 9 files changed, 88 insertions(+), 49 deletions(-) create mode 100644 internal/dev_server/sdk/store_facade.go diff --git a/internal/dev_server/adapters/sdk.go b/internal/dev_server/adapters/sdk.go index bdca9e7e..6fcfe881 100644 --- a/internal/dev_server/adapters/sdk.go +++ b/internal/dev_server/adapters/sdk.go @@ -6,6 +6,7 @@ import ( "time" "github.com/launchdarkly/go-sdk-common/v3/ldcontext" + "github.com/launchdarkly/go-sdk-common/v3/ldlog" ldsdk "github.com/launchdarkly/go-server-sdk/v7" "github.com/launchdarkly/go-server-sdk/v7/interfaces/flagstate" "github.com/launchdarkly/go-server-sdk/v7/ldcomponents" @@ -35,6 +36,7 @@ func (s Sdk) GetAllFlagsState(ctx context.Context, ldContext ldcontext.Context, config := ldsdk.Config{ DiagnosticOptOut: true, Events: ldcomponents.NoEvents(), + Logging: ldcomponents.Logging().MinLevel(ldlog.Debug), } if s.streamingUrl != "" { config.ServiceEndpoints.Streaming = s.streamingUrl diff --git a/internal/dev_server/api/server.go b/internal/dev_server/api/server.go index 20796bdd..bc12891a 100644 --- a/internal/dev_server/api/server.go +++ b/internal/dev_server/api/server.go @@ -4,7 +4,6 @@ import ( "context" "errors" - "github.com/launchdarkly/ldcli/internal/dev_server/db" "github.com/launchdarkly/ldcli/internal/dev_server/model" ) @@ -224,7 +223,7 @@ func (s Server) DeleteDevProjectsProjectKeyOverridesFlagKey(ctx context.Context, store := model.StoreFromContext(ctx) err := store.DeleteOverride(ctx, request.ProjectKey, request.FlagKey) if err != nil { - if errors.Is(err, db.ErrNotFound) { + if errors.Is(err, model.ErrNotFound) { return DeleteDevProjectsProjectKeyOverridesFlagKey404Response{}, nil } return nil, err diff --git a/internal/dev_server/db/sqlite.go b/internal/dev_server/db/sqlite.go index 0d3f416a..22063ac6 100644 --- a/internal/dev_server/db/sqlite.go +++ b/internal/dev_server/db/sqlite.go @@ -12,8 +12,6 @@ import ( "github.com/launchdarkly/ldcli/internal/dev_server/model" ) -var ErrNotFound = errors.New("not found") - type Sqlite struct { database *sql.DB } @@ -47,8 +45,8 @@ func (s Sqlite) GetDevProject(ctx context.Context, key string) (*model.Project, `, key) if err := row.Scan(&project.Key, &project.SourceEnvironmentKey, &contextData, &project.LastSyncTime, &flagStateData); err != nil { - if err == sql.ErrNoRows { - return nil, nil // No project found with the given key + if errors.Is(err, sql.ErrNoRows) { + return nil, errors.Wrapf(model.ErrNotFound, "no project found with key, '%s'", key) } return nil, err } @@ -77,6 +75,9 @@ func (s Sqlite) UpdateProject(ctx context.Context, project model.Project) (bool, SET flag_state = ?, last_sync_time = ?, context=? WHERE key = ?; `, flagsStateJson, project.LastSyncTime, project.Context.JSONString(), project.Key) + if err != nil { + return false, errors.Wrap(err, "unable to execute update project") + } rowsAffected, err := result.RowsAffected() if err != nil { @@ -213,7 +214,7 @@ func (s Sqlite) DeleteOverride(ctx context.Context, projectKey, flagKey string) return err } if rowsAffected == 0 { - return ErrNotFound + return model.ErrNotFound } return nil diff --git a/internal/dev_server/model/store.go b/internal/dev_server/model/store.go index 5eb0d810..09ab378d 100644 --- a/internal/dev_server/model/store.go +++ b/internal/dev_server/model/store.go @@ -5,6 +5,7 @@ import ( "net/http" "github.com/gorilla/mux" + "github.com/pkg/errors" ) type ctxKey string @@ -14,6 +15,7 @@ const ctxKeyStore = ctxKey("model.Store") type Store interface { DeleteOverride(ctx context.Context, projectKey, flagKey string) error GetDevProjects(ctx context.Context) ([]string, error) + // GetDevProject fetches the project based on the projectKey. If it doesn't exist, ErrNotFound is returned GetDevProject(ctx context.Context, projectKey string) (*Project, error) UpdateProject(ctx context.Context, project Project) (bool, error) DeleteDevProject(ctx context.Context, projectKey string) (bool, error) @@ -40,3 +42,5 @@ func StoreMiddleware(store Store) mux.MiddlewareFunc { }) } } + +var ErrNotFound = errors.New("not found") diff --git a/internal/dev_server/sdk/get_client_flags.go b/internal/dev_server/sdk/get_client_flags.go index 8bb8ecbf..acbc14a6 100644 --- a/internal/dev_server/sdk/get_client_flags.go +++ b/internal/dev_server/sdk/get_client_flags.go @@ -4,29 +4,25 @@ import ( "encoding/json" "net/http" - "github.com/launchdarkly/ldcli/internal/dev_server/model" "github.com/pkg/errors" ) func GetClientFlags(w http.ResponseWriter, r *http.Request) { ctx := r.Context() - store := model.StoreFromContext(ctx) - projectKey := GetProjectKeyFromContext(ctx) - project, err := store.GetDevProject(ctx, projectKey) + allFlags, err := GetAllFlagsFromContext(ctx) if err != nil { - panic(errors.Wrap(err, "unable to get dev project")) - } - allFlags, err := project.GetFlagStateWithOverridesForProject(ctx) - if err != nil { - panic(errors.Wrap(err, "failed to get flag state")) + WriteError(ctx, w, errors.Wrap(err, "failed to get flag state")) + return } jsonBody, err := json.Marshal(allFlags) if err != nil { - panic(errors.Wrap(err, "failed to marshal flag state")) + WriteError(ctx, w, errors.Wrap(err, "failed to marshal flag state")) + return } w.Header().Set("Content-Type", "application/json") _, err = w.Write(jsonBody) if err != nil { - panic(errors.Wrap(err, "unable to write response")) + WriteError(ctx, w, errors.Wrap(err, "unable to write response")) + return } } diff --git a/internal/dev_server/sdk/get_server_flags.go b/internal/dev_server/sdk/get_server_flags.go index ce2749c9..87aa056c 100644 --- a/internal/dev_server/sdk/get_server_flags.go +++ b/internal/dev_server/sdk/get_server_flags.go @@ -5,35 +5,34 @@ import ( "net/http" "github.com/gorilla/mux" - "github.com/launchdarkly/ldcli/internal/dev_server/model" "github.com/pkg/errors" ) func GetServerFlags(w http.ResponseWriter, r *http.Request) { ctx := r.Context() - store := model.StoreFromContext(ctx) - projectKey := GetProjectKeyFromContext(ctx) - project, err := store.GetDevProject(ctx, projectKey) + allFlags, err := GetAllFlagsFromContext(ctx) if err != nil { - panic(errors.Wrap(err, "unable to get dev project")) - } - allFlags, err := project.GetFlagStateWithOverridesForProject(ctx) - if err != nil { - panic(errors.Wrap(err, "failed to get flag state")) + WriteError(ctx, w, errors.Wrap(err, "failed to get flag state")) + return } var body interface{} if flagKey, ok := mux.Vars(r)["flagKey"]; ok { - body = ServerFlagsFromFlagsState(allFlags)[flagKey] + body, ok = ServerFlagsFromFlagsState(allFlags)[flagKey] + if !ok { + http.Error(w, "flag not found", http.StatusNotFound) + } } else { body = ServerAllPayloadFromFlagsState(allFlags) } jsonBody, err := json.Marshal(body) if err != nil { - panic(errors.Wrap(err, "failed to marshal flag state")) + WriteError(ctx, w, errors.Wrap(err, "failed to marshal flag state")) + return } w.Header().Set("Content-Type", "application/json") _, err = w.Write(jsonBody) if err != nil { - panic(errors.Wrap(err, "unable to write response")) + WriteError(ctx, w, errors.Wrap(err, "unable to write response")) + return } } diff --git a/internal/dev_server/sdk/store_facade.go b/internal/dev_server/sdk/store_facade.go new file mode 100644 index 00000000..08c629ef --- /dev/null +++ b/internal/dev_server/sdk/store_facade.go @@ -0,0 +1,42 @@ +package sdk + +import ( + "context" + "fmt" + "log" + "net/http" + + "github.com/launchdarkly/ldcli/internal/dev_server/model" + "github.com/pkg/errors" +) + +// WriteError writes out a given error if it's known or panics if it isn't. +// Two assumptions it's making +// - a panic handling middleware is in use +// - This is in the context of flag delivery which has pretty consistent semantics for what's an error across handlers. +func WriteError(ctx context.Context, w http.ResponseWriter, err error) { + switch { + case errors.Is(err, model.ErrNotFound): + projectKey := GetProjectKeyFromContext(ctx) + message := fmt.Sprintf("project, %s, not found", projectKey) + log.Println(message) + log.Printf("To add your project to the dev server, call `ldcli dev-server add-project --project %s --source {SOURCE_ENV_KEY}", projectKey) + http.Error(w, message, http.StatusNotFound) + case err != nil: + panic(err) + } +} + +func GetAllFlagsFromContext(ctx context.Context) (model.FlagsState, error) { + store := model.StoreFromContext(ctx) + projectKey := GetProjectKeyFromContext(ctx) + project, err := store.GetDevProject(ctx, projectKey) + if err != nil { + return model.FlagsState{}, errors.Wrap(err, "unable to get dev project") + } + allFlags, err := project.GetFlagStateWithOverridesForProject(ctx) + if err != nil { + return model.FlagsState{}, errors.Wrap(err, "unable to get flags for project") + } + return allFlags, nil +} diff --git a/internal/dev_server/sdk/stream_client_flags.go b/internal/dev_server/sdk/stream_client_flags.go index 62ef28fd..e5fd953c 100644 --- a/internal/dev_server/sdk/stream_client_flags.go +++ b/internal/dev_server/sdk/stream_client_flags.go @@ -12,22 +12,19 @@ import ( func StreamClientFlags(w http.ResponseWriter, r *http.Request) { ctx := r.Context() - store := model.StoreFromContext(ctx) - projectKey := GetProjectKeyFromContext(ctx) - project, err := store.GetDevProject(ctx, projectKey) - if err != nil { - panic(errors.Wrap(err, "unable to get dev project")) - } - allFlags, err := project.GetFlagStateWithOverridesForProject(ctx) + allFlags, err := GetAllFlagsFromContext(ctx) if err != nil { - panic(errors.Wrap(err, "failed to get flag state")) + WriteError(ctx, w, errors.Wrap(err, "failed to get flag state")) + return } jsonBody, err := json.Marshal(allFlags) if err != nil { - panic(errors.Wrap(err, "failed to marshal flag state")) + WriteError(ctx, w, errors.Wrap(err, "failed to marshal flag state")) + return } updateChan, doneChan := OpenStream(w, r.Context().Done(), Message{"put", jsonBody}) // TODO Wireup updateChan defer close(updateChan) + projectKey := GetProjectKeyFromContext(ctx) observer := clientFlagsObserver{updateChan, projectKey} observers := model.GetObserversFromContext(ctx) observerId := observers.RegisterObserver(observer) @@ -39,7 +36,8 @@ func StreamClientFlags(w http.ResponseWriter, r *http.Request) { }() err = <-doneChan if err != nil { - panic(errors.Wrap(err, "stream failure")) + WriteError(ctx, w, errors.Wrap(err, "stream failure")) + return } } diff --git a/internal/dev_server/sdk/stream_server_flags.go b/internal/dev_server/sdk/stream_server_flags.go index 6f70731b..45b41613 100644 --- a/internal/dev_server/sdk/stream_server_flags.go +++ b/internal/dev_server/sdk/stream_server_flags.go @@ -12,20 +12,17 @@ import ( func StreamServerAllPayload(w http.ResponseWriter, r *http.Request) { ctx := r.Context() - store := model.StoreFromContext(ctx) projectKey := GetProjectKeyFromContext(ctx) - project, err := store.GetDevProject(ctx, projectKey) + allFlags, err := GetAllFlagsFromContext(ctx) if err != nil { - panic(errors.Wrap(err, "unable to get dev project")) - } - allFlags, err := project.GetFlagStateWithOverridesForProject(ctx) - if err != nil { - panic(errors.Wrap(err, "failed to get flag state")) + WriteError(ctx, w, errors.Wrap(err, "failed to get flag state")) + return } serverFlags := ServerAllPayloadFromFlagsState(allFlags) jsonBody, err := json.Marshal(serverFlags) if err != nil { - panic(errors.Wrap(err, "failed to marshal flag state")) + WriteError(ctx, w, errors.Wrap(err, "failed to marshal flag state")) + return } updateChan, doneChan := OpenStream(w, r.Context().Done(), Message{"put", jsonBody}) defer close(updateChan) @@ -40,7 +37,8 @@ func StreamServerAllPayload(w http.ResponseWriter, r *http.Request) { }() err = <-doneChan if err != nil { - panic(errors.Wrap(err, "stream failure")) + WriteError(ctx, w, errors.Wrap(err, "stream failure")) + return } } From 46eeab080f99befb315fe7b60f9ddb48d549d5ba Mon Sep 17 00:00:00 2001 From: Mike Zorn Date: Wed, 3 Jul 2024 10:06:31 -0700 Subject: [PATCH 53/79] fix: integration tests for go sdk flag delivery (#362) This adds some integration tests for go server SDK & Dev server. --- go.mod | 13 +- go.sum | 26 +- internal/dev_server/adapters/api.go | 11 +- internal/dev_server/adapters/mocks/api.go | 55 + internal/dev_server/adapters/mocks/sdk.go | 57 + internal/dev_server/adapters/sdk.go | 11 +- internal/dev_server/api/server.go | 5 +- .../model/mock_observer/observer.go | 51 + internal/dev_server/model/observer.go | 2 + internal/dev_server/model/override.go | 13 +- internal/dev_server/sdk/go_sdk_test.go | 130 ++ tools.go | 1 + vendor/go.uber.org/mock/AUTHORS | 12 + vendor/go.uber.org/mock/LICENSE | 202 ++ vendor/go.uber.org/mock/gomock/call.go | 508 +++++ vendor/go.uber.org/mock/gomock/callset.go | 164 ++ vendor/go.uber.org/mock/gomock/controller.go | 318 +++ vendor/go.uber.org/mock/gomock/doc.go | 60 + vendor/go.uber.org/mock/gomock/matchers.go | 443 +++++ .../go.uber.org/mock/mockgen/generic_go118.go | 130 ++ .../mock/mockgen/generic_notgo118.go | 41 + vendor/go.uber.org/mock/mockgen/mockgen.go | 883 +++++++++ .../go.uber.org/mock/mockgen/model/model.go | 533 +++++ vendor/go.uber.org/mock/mockgen/parse.go | 805 ++++++++ vendor/go.uber.org/mock/mockgen/reflect.go | 256 +++ vendor/go.uber.org/mock/mockgen/version.go | 31 + vendor/golang.org/x/mod/modfile/print.go | 184 ++ vendor/golang.org/x/mod/modfile/read.go | 958 +++++++++ vendor/golang.org/x/mod/modfile/rule.go | 1766 +++++++++++++++++ vendor/golang.org/x/mod/modfile/work.go | 335 ++++ vendor/golang.org/x/mod/module/module.go | 2 + vendor/golang.org/x/sys/unix/mkerrors.sh | 2 + vendor/golang.org/x/sys/unix/zerrors_linux.go | 20 +- .../x/sys/unix/zerrors_linux_386.go | 1 + .../x/sys/unix/zerrors_linux_amd64.go | 1 + .../x/sys/unix/zerrors_linux_arm64.go | 1 + vendor/golang.org/x/sys/unix/ztypes_linux.go | 37 +- .../x/sys/windows/security_windows.go | 1 + .../x/sys/windows/zsyscall_windows.go | 9 + .../x/tools/internal/gocommand/invoke.go | 96 +- .../x/tools/internal/imports/fix.go | 28 +- vendor/modules.txt | 18 +- 42 files changed, 8153 insertions(+), 67 deletions(-) create mode 100644 internal/dev_server/adapters/mocks/api.go create mode 100644 internal/dev_server/adapters/mocks/sdk.go create mode 100644 internal/dev_server/model/mock_observer/observer.go create mode 100644 internal/dev_server/sdk/go_sdk_test.go create mode 100644 vendor/go.uber.org/mock/AUTHORS create mode 100644 vendor/go.uber.org/mock/LICENSE create mode 100644 vendor/go.uber.org/mock/gomock/call.go create mode 100644 vendor/go.uber.org/mock/gomock/callset.go create mode 100644 vendor/go.uber.org/mock/gomock/controller.go create mode 100644 vendor/go.uber.org/mock/gomock/doc.go create mode 100644 vendor/go.uber.org/mock/gomock/matchers.go create mode 100644 vendor/go.uber.org/mock/mockgen/generic_go118.go create mode 100644 vendor/go.uber.org/mock/mockgen/generic_notgo118.go create mode 100644 vendor/go.uber.org/mock/mockgen/mockgen.go create mode 100644 vendor/go.uber.org/mock/mockgen/model/model.go create mode 100644 vendor/go.uber.org/mock/mockgen/parse.go create mode 100644 vendor/go.uber.org/mock/mockgen/reflect.go create mode 100644 vendor/go.uber.org/mock/mockgen/version.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 3deadd32..ddbbe287 100644 --- a/go.mod +++ b/go.mod @@ -26,7 +26,8 @@ require ( github.com/spf13/pflag v1.0.5 github.com/spf13/viper v1.18.2 github.com/stretchr/testify v1.9.0 - golang.org/x/term v0.20.0 + go.uber.org/mock v0.4.0 + golang.org/x/term v0.21.0 gopkg.in/yaml.v3 v3.0.1 ) @@ -87,13 +88,13 @@ require ( go.uber.org/atomic v1.9.0 // indirect go.uber.org/multierr v1.9.0 // indirect golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect - golang.org/x/mod v0.17.0 // indirect - golang.org/x/net v0.25.0 // indirect + golang.org/x/mod v0.18.0 // indirect + golang.org/x/net v0.26.0 // indirect golang.org/x/oauth2 v0.15.0 // indirect golang.org/x/sync v0.7.0 // indirect - golang.org/x/sys v0.20.0 // indirect - golang.org/x/text v0.15.0 // indirect - golang.org/x/tools v0.21.0 // indirect + golang.org/x/sys v0.21.0 // indirect + golang.org/x/text v0.16.0 // indirect + golang.org/x/tools v0.22.0 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/protobuf v1.33.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect diff --git a/go.sum b/go.sum index 7165d731..d3ca4f84 100644 --- a/go.sum +++ b/go.sum @@ -321,6 +321,8 @@ go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU= +go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= @@ -360,8 +362,8 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= -golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0= +golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -389,8 +391,8 @@ golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81R golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20221002022538-bcab6841153b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= -golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= -golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= +golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -440,19 +442,19 @@ golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= -golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= +golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw= -golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= +golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= +golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= -golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -496,8 +498,8 @@ golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roY golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.21.0 h1:qc0xYgIbsSDt9EyWz05J5wfa7LOVW0YTLOXrqdLAWIw= -golang.org/x/tools v0.21.0/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA= +golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/internal/dev_server/adapters/api.go b/internal/dev_server/adapters/api.go index 9d4dfece..73681881 100644 --- a/internal/dev_server/adapters/api.go +++ b/internal/dev_server/adapters/api.go @@ -16,15 +16,20 @@ func GetApi(ctx context.Context) Api { return ctx.Value(ctxKeyApi).(Api) } -type Api struct { +//go:generate go run go.uber.org/mock/mockgen -destination mocks/api.go -package mocks . Api +type Api interface { + GetSdkKey(ctx context.Context, projectKey, environmentKey string) (string, error) +} + +type apiClientApi struct { apiClient ldapi.APIClient } func NewApi(client ldapi.APIClient) Api { - return Api{client} + return apiClientApi{client} } -func (a Api) GetSdkKey(ctx context.Context, projectKey, environmentKey string) (string, error) { +func (a apiClientApi) GetSdkKey(ctx context.Context, projectKey, environmentKey string) (string, error) { environment, _, err := a.apiClient.EnvironmentsApi.GetEnvironment(ctx, projectKey, environmentKey).Execute() if err != nil { return "", err diff --git a/internal/dev_server/adapters/mocks/api.go b/internal/dev_server/adapters/mocks/api.go new file mode 100644 index 00000000..efacb902 --- /dev/null +++ b/internal/dev_server/adapters/mocks/api.go @@ -0,0 +1,55 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/launchdarkly/ldcli/internal/dev_server/adapters (interfaces: Api) +// +// Generated by this command: +// +// mockgen -destination mocks/api.go -package mocks . Api +// + +// Package mocks is a generated GoMock package. +package mocks + +import ( + context "context" + reflect "reflect" + + gomock "go.uber.org/mock/gomock" +) + +// MockApi is a mock of Api interface. +type MockApi struct { + ctrl *gomock.Controller + recorder *MockApiMockRecorder +} + +// MockApiMockRecorder is the mock recorder for MockApi. +type MockApiMockRecorder struct { + mock *MockApi +} + +// NewMockApi creates a new mock instance. +func NewMockApi(ctrl *gomock.Controller) *MockApi { + mock := &MockApi{ctrl: ctrl} + mock.recorder = &MockApiMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockApi) EXPECT() *MockApiMockRecorder { + return m.recorder +} + +// GetSdkKey mocks base method. +func (m *MockApi) GetSdkKey(arg0 context.Context, arg1, arg2 string) (string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetSdkKey", arg0, arg1, arg2) + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetSdkKey indicates an expected call of GetSdkKey. +func (mr *MockApiMockRecorder) GetSdkKey(arg0, arg1, arg2 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSdkKey", reflect.TypeOf((*MockApi)(nil).GetSdkKey), arg0, arg1, arg2) +} diff --git a/internal/dev_server/adapters/mocks/sdk.go b/internal/dev_server/adapters/mocks/sdk.go new file mode 100644 index 00000000..80125d01 --- /dev/null +++ b/internal/dev_server/adapters/mocks/sdk.go @@ -0,0 +1,57 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/launchdarkly/ldcli/internal/dev_server/adapters (interfaces: Sdk) +// +// Generated by this command: +// +// mockgen -destination mocks/sdk.go -package mocks . Sdk +// + +// Package mocks is a generated GoMock package. +package mocks + +import ( + context "context" + reflect "reflect" + + ldcontext "github.com/launchdarkly/go-sdk-common/v3/ldcontext" + flagstate "github.com/launchdarkly/go-server-sdk/v7/interfaces/flagstate" + gomock "go.uber.org/mock/gomock" +) + +// MockSdk is a mock of Sdk interface. +type MockSdk struct { + ctrl *gomock.Controller + recorder *MockSdkMockRecorder +} + +// MockSdkMockRecorder is the mock recorder for MockSdk. +type MockSdkMockRecorder struct { + mock *MockSdk +} + +// NewMockSdk creates a new mock instance. +func NewMockSdk(ctrl *gomock.Controller) *MockSdk { + mock := &MockSdk{ctrl: ctrl} + mock.recorder = &MockSdkMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockSdk) EXPECT() *MockSdkMockRecorder { + return m.recorder +} + +// GetAllFlagsState mocks base method. +func (m *MockSdk) GetAllFlagsState(arg0 context.Context, arg1 ldcontext.Context, arg2 string) (flagstate.AllFlags, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetAllFlagsState", arg0, arg1, arg2) + ret0, _ := ret[0].(flagstate.AllFlags) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetAllFlagsState indicates an expected call of GetAllFlagsState. +func (mr *MockSdkMockRecorder) GetAllFlagsState(arg0, arg1, arg2 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAllFlagsState", reflect.TypeOf((*MockSdk)(nil).GetAllFlagsState), arg0, arg1, arg2) +} diff --git a/internal/dev_server/adapters/sdk.go b/internal/dev_server/adapters/sdk.go index 6fcfe881..a61b7653 100644 --- a/internal/dev_server/adapters/sdk.go +++ b/internal/dev_server/adapters/sdk.go @@ -22,17 +22,22 @@ func GetSdk(ctx context.Context) Sdk { return ctx.Value(ctxKeySdk).(Sdk) } -type Sdk struct { +//go:generate go run go.uber.org/mock/mockgen -destination mocks/sdk.go -package mocks . Sdk +type Sdk interface { + GetAllFlagsState(ctx context.Context, ldContext ldcontext.Context, sdkKey string) (flagstate.AllFlags, error) +} + +type streamingSdk struct { streamingUrl string } func newSdk(streamingUrl string) Sdk { - return Sdk{ + return streamingSdk{ streamingUrl: streamingUrl, } } -func (s Sdk) GetAllFlagsState(ctx context.Context, ldContext ldcontext.Context, sdkKey string) (flagstate.AllFlags, error) { +func (s streamingSdk) GetAllFlagsState(ctx context.Context, ldContext ldcontext.Context, sdkKey string) (flagstate.AllFlags, error) { config := ldsdk.Config{ DiagnosticOptOut: true, Events: ldcomponents.NoEvents(), diff --git a/internal/dev_server/api/server.go b/internal/dev_server/api/server.go index bc12891a..76850917 100644 --- a/internal/dev_server/api/server.go +++ b/internal/dev_server/api/server.go @@ -232,7 +232,10 @@ func (s Server) DeleteDevProjectsProjectKeyOverridesFlagKey(ctx context.Context, } func (s Server) PutDevProjectsProjectKeyOverridesFlagKey(ctx context.Context, request PutDevProjectsProjectKeyOverridesFlagKeyRequestObject) (PutDevProjectsProjectKeyOverridesFlagKeyResponseObject, error) { - override, err := model.UpsertOverride(ctx, request.ProjectKey, request.FlagKey, request.Body.String()) + if request.Body == nil { + return nil, errors.New("empty override body") + } + override, err := model.UpsertOverride(ctx, request.ProjectKey, request.FlagKey, *request.Body) if err != nil { return nil, err } diff --git a/internal/dev_server/model/mock_observer/observer.go b/internal/dev_server/model/mock_observer/observer.go new file mode 100644 index 00000000..62c67fcf --- /dev/null +++ b/internal/dev_server/model/mock_observer/observer.go @@ -0,0 +1,51 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/launchdarkly/ldcli/internal/dev_server/model (interfaces: Observer) +// +// Generated by this command: +// +// mockgen -destination mock_observer/observer.go -package mock_observer . Observer +// + +// Package mock_observer is a generated GoMock package. +package mock_observer + +import ( + reflect "reflect" + + gomock "go.uber.org/mock/gomock" +) + +// MockObserver is a mock of Observer interface. +type MockObserver struct { + ctrl *gomock.Controller + recorder *MockObserverMockRecorder +} + +// MockObserverMockRecorder is the mock recorder for MockObserver. +type MockObserverMockRecorder struct { + mock *MockObserver +} + +// NewMockObserver creates a new mock instance. +func NewMockObserver(ctrl *gomock.Controller) *MockObserver { + mock := &MockObserver{ctrl: ctrl} + mock.recorder = &MockObserverMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockObserver) EXPECT() *MockObserverMockRecorder { + return m.recorder +} + +// Handle mocks base method. +func (m *MockObserver) Handle(arg0 any) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "Handle", arg0) +} + +// Handle indicates an expected call of Handle. +func (mr *MockObserverMockRecorder) Handle(arg0 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Handle", reflect.TypeOf((*MockObserver)(nil).Handle), arg0) +} diff --git a/internal/dev_server/model/observer.go b/internal/dev_server/model/observer.go index 1728c10c..480a81d1 100644 --- a/internal/dev_server/model/observer.go +++ b/internal/dev_server/model/observer.go @@ -6,6 +6,8 @@ import ( "github.com/google/uuid" ) +//go:generate go run go.uber.org/mock/mockgen -destination mock_observer/observer.go -package mock_observer . Observer + type Observer interface { Handle(interface{}) } diff --git a/internal/dev_server/model/override.go b/internal/dev_server/model/override.go index 6f4241bf..523963b7 100644 --- a/internal/dev_server/model/override.go +++ b/internal/dev_server/model/override.go @@ -2,7 +2,6 @@ package model import ( "context" - "encoding/json" "github.com/launchdarkly/go-sdk-common/v3/ldvalue" "github.com/pkg/errors" @@ -22,24 +21,18 @@ type UpsertOverrideEvent struct { FlagState FlagState } -func UpsertOverride(ctx context.Context, projectKey, flagKey, value string) (Override, error) { +func UpsertOverride(ctx context.Context, projectKey, flagKey string, value ldvalue.Value) (Override, error) { // TODO: validate flag exists within project, + if the flag type matches - var val ldvalue.Value - err := json.Unmarshal([]byte(value), &val) - if err != nil { - return Override{}, err - } - override := Override{ ProjectKey: projectKey, FlagKey: flagKey, - Value: val, + Value: value, Active: true, Version: 1, } store := StoreFromContext(ctx) - override, err = store.UpsertOverride(ctx, override) + override, err := store.UpsertOverride(ctx, override) if err != nil { return Override{}, err } diff --git a/internal/dev_server/sdk/go_sdk_test.go b/internal/dev_server/sdk/go_sdk_test.go new file mode 100644 index 00000000..a97e5c2c --- /dev/null +++ b/internal/dev_server/sdk/go_sdk_test.go @@ -0,0 +1,130 @@ +package sdk + +import ( + "context" + "fmt" + "net/http/httptest" + "os" + "testing" + "time" + + "github.com/gorilla/mux" + "github.com/launchdarkly/go-sdk-common/v3/ldcontext" + "github.com/launchdarkly/go-sdk-common/v3/ldvalue" + ldclient "github.com/launchdarkly/go-server-sdk/v7" + "github.com/launchdarkly/go-server-sdk/v7/interfaces/flagstate" + "github.com/launchdarkly/ldcli/internal/dev_server/adapters" + "github.com/launchdarkly/ldcli/internal/dev_server/adapters/mocks" + "github.com/launchdarkly/ldcli/internal/dev_server/db" + "github.com/launchdarkly/ldcli/internal/dev_server/model" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" +) + +// TestSdkRoutesViaGoSDK is an integration test. It hooks up a real go SDK to our SDK routes and makes changes to the +// model using the model methods directly. It also uses a real sqlite store. Goal of these tests are to ensure that the +// happy path works end to end (almost -- API handlers are omitted, but they are thin wrappers over the model). +func TestSDKRoutesViaGoSDK(t *testing.T) { + const projectKey = "test-project" + const environmentKey = "test-environment" + const testSdkKey = "1234" + + // Wire up model dependencies to context + ctx := context.Background() + store, err := db.NewSqlite(ctx, "test.db") + require.NoError(t, err) + defer func() { + require.NoError(t, os.Remove("test.db")) + }() + ctx = model.ContextWithStore(ctx, store) + observers := model.NewObservers() + ctx = model.SetObserversOnContext(ctx, observers) + // Mock the external LD APIs + mockController := gomock.NewController(t) + api := mocks.NewMockApi(mockController) + ctx = adapters.WithApi(ctx, api) + sdk := mocks.NewMockSdk(mockController) + ctx = adapters.WithSdk(ctx, sdk) + + api.EXPECT().GetSdkKey(gomock.Any(), projectKey, environmentKey).Return(testSdkKey, nil).AnyTimes() + + // Wire up sdk routes in test server + router := mux.NewRouter() + router.Use(model.StoreMiddleware(store)) + router.Use(model.ObserversMiddleware(observers)) + BindRoutes(router) + require.NoError(t, err) + testServer := httptest.NewServer(router) + + // Initialize project with all kinds of flags + allFlags := flagstate.NewAllFlagsBuilder(). + AddFlag("boolFlag", flagstate.FlagState{Value: ldvalue.Bool(true)}). + AddFlag("stringFlag", flagstate.FlagState{Value: ldvalue.String("cool")}). + AddFlag("intFlag", flagstate.FlagState{Value: ldvalue.Int(123)}). + AddFlag("doubleFlag", flagstate.FlagState{Value: ldvalue.Float64(99.99)}). + AddFlag("jsonFlag", flagstate.FlagState{Value: ldvalue.CopyArbitraryValue(map[string]any{"cat": "hat"})}). + Build() + + sdk.EXPECT().GetAllFlagsState(gomock.Any(), gomock.Any(), testSdkKey).Return(allFlags, nil) + _, err = model.CreateProject(ctx, projectKey, environmentKey, nil) + require.NoError(t, err) + + // Configure go SDK to use test server + ldConfig := ldclient.Config{} + ldConfig.ServiceEndpoints.Streaming = testServer.URL + ldConfig.ServiceEndpoints.Events = testServer.URL + ldConfig.ServiceEndpoints.Polling = testServer.URL + ld, err := ldclient.MakeCustomClient(projectKey, ldConfig, time.Second) + require.NoError(t, err) + + ldContext := ldcontext.New(t.Name()) + + t.Run("bool flag is true in fresh environment", func(t *testing.T) { + val, err := ld.BoolVariation("boolFlag", ldContext, false) + require.NoError(t, err) + assert.True(t, val, "boolean variation is expected value") + }) + + t.Run("string flag is cool in fresh environment", func(t *testing.T) { + val, err := ld.StringVariation("stringFlag", ldContext, "bad") + require.NoError(t, err) + assert.Equal(t, "cool", val) + }) + + t.Run("int flag is 123 in fresh environment", func(t *testing.T) { + val, err := ld.IntVariation("intFlag", ldContext, 0) + require.NoError(t, err) + assert.Equal(t, 123, val) + }) + + t.Run("doubleFlag is 4 9s in fresh environment", func(t *testing.T) { + val, err := ld.Float64Variation("doubleFlag", ldContext, 0) + require.NoError(t, err) + assert.Equal(t, 99.99, val) + }) + + t.Run("jsonFlag is cat :hat in fresh environment", func(t *testing.T) { + val, err := ld.JSONVariation("jsonFlag", ldContext, ldvalue.Null()) + require.NoError(t, err) + assert.Equal(t, map[string]any{"cat": "hat"}, val.AsArbitraryValue()) + }) + + updates := map[string]ldvalue.Value{ + "boolFlag": ldvalue.Bool(false), + "stringFlag": ldvalue.String("drool"), + "intFlag": ldvalue.Int(456), + "doubleFlag": ldvalue.Float64(88.88), + "jsonFlag": ldvalue.CopyArbitraryValue(map[string]any{"tortoise": "shell"}), + } + for flagKey, value := range updates { + t.Run(fmt.Sprintf("%s is %v after override", flagKey, value), func(t *testing.T) { + flagUpdateChan := ld.GetFlagTracker().AddFlagValueChangeListener(flagKey, ldContext, ldvalue.String("uh-oh")) + defer ld.GetFlagTracker().RemoveFlagValueChangeListener(flagUpdateChan) + _, err := model.UpsertOverride(ctx, projectKey, flagKey, value) + require.NoError(t, err) + flagUpdate := <-flagUpdateChan + assert.Equal(t, value.AsArbitraryValue(), flagUpdate.NewValue.AsArbitraryValue()) + }) + } +} diff --git a/tools.go b/tools.go index 461b5b53..a9d50a35 100644 --- a/tools.go +++ b/tools.go @@ -3,3 +3,4 @@ package main import _ "github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen" +import _ "go.uber.org/mock/mockgen" diff --git a/vendor/go.uber.org/mock/AUTHORS b/vendor/go.uber.org/mock/AUTHORS new file mode 100644 index 00000000..660b8ccc --- /dev/null +++ b/vendor/go.uber.org/mock/AUTHORS @@ -0,0 +1,12 @@ +# This is the official list of GoMock authors for copyright purposes. +# This file is distinct from the CONTRIBUTORS files. +# See the latter for an explanation. + +# Names should be added to this file as +# Name or Organization +# The email address is not required for organizations. + +# Please keep the list sorted. + +Alex Reece +Google Inc. diff --git a/vendor/go.uber.org/mock/LICENSE b/vendor/go.uber.org/mock/LICENSE new file mode 100644 index 00000000..d6456956 --- /dev/null +++ b/vendor/go.uber.org/mock/LICENSE @@ -0,0 +1,202 @@ + + 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/go.uber.org/mock/gomock/call.go b/vendor/go.uber.org/mock/gomock/call.go new file mode 100644 index 00000000..e1ea8263 --- /dev/null +++ b/vendor/go.uber.org/mock/gomock/call.go @@ -0,0 +1,508 @@ +// Copyright 2010 Google Inc. +// +// 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. + +package gomock + +import ( + "fmt" + "reflect" + "strconv" + "strings" +) + +// Call represents an expected call to a mock. +type Call struct { + t TestHelper // for triggering test failures on invalid call setup + + receiver any // the receiver of the method call + method string // the name of the method + methodType reflect.Type // the type of the method + args []Matcher // the args + origin string // file and line number of call setup + + preReqs []*Call // prerequisite calls + + // Expectations + minCalls, maxCalls int + + numCalls int // actual number made + + // actions are called when this Call is called. Each action gets the args and + // can set the return values by returning a non-nil slice. Actions run in the + // order they are created. + actions []func([]any) []any +} + +// newCall creates a *Call. It requires the method type in order to support +// unexported methods. +func newCall(t TestHelper, receiver any, method string, methodType reflect.Type, args ...any) *Call { + t.Helper() + + // TODO: check arity, types. + mArgs := make([]Matcher, len(args)) + for i, arg := range args { + if m, ok := arg.(Matcher); ok { + mArgs[i] = m + } else if arg == nil { + // Handle nil specially so that passing a nil interface value + // will match the typed nils of concrete args. + mArgs[i] = Nil() + } else { + mArgs[i] = Eq(arg) + } + } + + // callerInfo's skip should be updated if the number of calls between the user's test + // and this line changes, i.e. this code is wrapped in another anonymous function. + // 0 is us, 1 is RecordCallWithMethodType(), 2 is the generated recorder, and 3 is the user's test. + origin := callerInfo(3) + actions := []func([]any) []any{func([]any) []any { + // Synthesize the zero value for each of the return args' types. + rets := make([]any, methodType.NumOut()) + for i := 0; i < methodType.NumOut(); i++ { + rets[i] = reflect.Zero(methodType.Out(i)).Interface() + } + return rets + }} + return &Call{t: t, receiver: receiver, method: method, methodType: methodType, + args: mArgs, origin: origin, minCalls: 1, maxCalls: 1, actions: actions} +} + +// AnyTimes allows the expectation to be called 0 or more times +func (c *Call) AnyTimes() *Call { + c.minCalls, c.maxCalls = 0, 1e8 // close enough to infinity + return c +} + +// MinTimes requires the call to occur at least n times. If AnyTimes or MaxTimes have not been called or if MaxTimes +// was previously called with 1, MinTimes also sets the maximum number of calls to infinity. +func (c *Call) MinTimes(n int) *Call { + c.minCalls = n + if c.maxCalls == 1 { + c.maxCalls = 1e8 + } + return c +} + +// MaxTimes limits the number of calls to n times. If AnyTimes or MinTimes have not been called or if MinTimes was +// previously called with 1, MaxTimes also sets the minimum number of calls to 0. +func (c *Call) MaxTimes(n int) *Call { + c.maxCalls = n + if c.minCalls == 1 { + c.minCalls = 0 + } + return c +} + +// DoAndReturn declares the action to run when the call is matched. +// The return values from this function are returned by the mocked function. +// It takes an any argument to support n-arity functions. +// The anonymous function must match the function signature mocked method. +func (c *Call) DoAndReturn(f any) *Call { + // TODO: Check arity and types here, rather than dying badly elsewhere. + v := reflect.ValueOf(f) + + c.addAction(func(args []any) []any { + c.t.Helper() + ft := v.Type() + if c.methodType.NumIn() != ft.NumIn() { + if ft.IsVariadic() { + c.t.Fatalf("wrong number of arguments in DoAndReturn func for %T.%v The function signature must match the mocked method, a variadic function cannot be used.", + c.receiver, c.method) + } else { + c.t.Fatalf("wrong number of arguments in DoAndReturn func for %T.%v: got %d, want %d [%s]", + c.receiver, c.method, ft.NumIn(), c.methodType.NumIn(), c.origin) + } + return nil + } + vArgs := make([]reflect.Value, len(args)) + for i := 0; i < len(args); i++ { + if args[i] != nil { + vArgs[i] = reflect.ValueOf(args[i]) + } else { + // Use the zero value for the arg. + vArgs[i] = reflect.Zero(ft.In(i)) + } + } + vRets := v.Call(vArgs) + rets := make([]any, len(vRets)) + for i, ret := range vRets { + rets[i] = ret.Interface() + } + return rets + }) + return c +} + +// Do declares the action to run when the call is matched. The function's +// return values are ignored to retain backward compatibility. To use the +// return values call DoAndReturn. +// It takes an any argument to support n-arity functions. +// The anonymous function must match the function signature mocked method. +func (c *Call) Do(f any) *Call { + // TODO: Check arity and types here, rather than dying badly elsewhere. + v := reflect.ValueOf(f) + + c.addAction(func(args []any) []any { + c.t.Helper() + ft := v.Type() + if c.methodType.NumIn() != ft.NumIn() { + if ft.IsVariadic() { + c.t.Fatalf("wrong number of arguments in Do func for %T.%v The function signature must match the mocked method, a variadic function cannot be used.", + c.receiver, c.method) + } else { + c.t.Fatalf("wrong number of arguments in Do func for %T.%v: got %d, want %d [%s]", + c.receiver, c.method, ft.NumIn(), c.methodType.NumIn(), c.origin) + } + return nil + } + vArgs := make([]reflect.Value, len(args)) + for i := 0; i < len(args); i++ { + if args[i] != nil { + vArgs[i] = reflect.ValueOf(args[i]) + } else { + // Use the zero value for the arg. + vArgs[i] = reflect.Zero(ft.In(i)) + } + } + v.Call(vArgs) + return nil + }) + return c +} + +// Return declares the values to be returned by the mocked function call. +func (c *Call) Return(rets ...any) *Call { + c.t.Helper() + + mt := c.methodType + if len(rets) != mt.NumOut() { + c.t.Fatalf("wrong number of arguments to Return for %T.%v: got %d, want %d [%s]", + c.receiver, c.method, len(rets), mt.NumOut(), c.origin) + } + for i, ret := range rets { + if got, want := reflect.TypeOf(ret), mt.Out(i); got == want { + // Identical types; nothing to do. + } else if got == nil { + // Nil needs special handling. + switch want.Kind() { + case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice: + // ok + default: + c.t.Fatalf("argument %d to Return for %T.%v is nil, but %v is not nillable [%s]", + i, c.receiver, c.method, want, c.origin) + } + } else if got.AssignableTo(want) { + // Assignable type relation. Make the assignment now so that the generated code + // can return the values with a type assertion. + v := reflect.New(want).Elem() + v.Set(reflect.ValueOf(ret)) + rets[i] = v.Interface() + } else { + c.t.Fatalf("wrong type of argument %d to Return for %T.%v: %v is not assignable to %v [%s]", + i, c.receiver, c.method, got, want, c.origin) + } + } + + c.addAction(func([]any) []any { + return rets + }) + + return c +} + +// Times declares the exact number of times a function call is expected to be executed. +func (c *Call) Times(n int) *Call { + c.minCalls, c.maxCalls = n, n + return c +} + +// SetArg declares an action that will set the nth argument's value, +// indirected through a pointer. Or, in the case of a slice and map, SetArg +// will copy value's elements/key-value pairs into the nth argument. +func (c *Call) SetArg(n int, value any) *Call { + c.t.Helper() + + mt := c.methodType + // TODO: This will break on variadic methods. + // We will need to check those at invocation time. + if n < 0 || n >= mt.NumIn() { + c.t.Fatalf("SetArg(%d, ...) called for a method with %d args [%s]", + n, mt.NumIn(), c.origin) + } + // Permit setting argument through an interface. + // In the interface case, we don't (nay, can't) check the type here. + at := mt.In(n) + switch at.Kind() { + case reflect.Ptr: + dt := at.Elem() + if vt := reflect.TypeOf(value); !vt.AssignableTo(dt) { + c.t.Fatalf("SetArg(%d, ...) argument is a %v, not assignable to %v [%s]", + n, vt, dt, c.origin) + } + case reflect.Interface: + // nothing to do + case reflect.Slice: + // nothing to do + case reflect.Map: + // nothing to do + default: + c.t.Fatalf("SetArg(%d, ...) referring to argument of non-pointer non-interface non-slice non-map type %v [%s]", + n, at, c.origin) + } + + c.addAction(func(args []any) []any { + v := reflect.ValueOf(value) + switch reflect.TypeOf(args[n]).Kind() { + case reflect.Slice: + setSlice(args[n], v) + case reflect.Map: + setMap(args[n], v) + default: + reflect.ValueOf(args[n]).Elem().Set(v) + } + return nil + }) + return c +} + +// isPreReq returns true if other is a direct or indirect prerequisite to c. +func (c *Call) isPreReq(other *Call) bool { + for _, preReq := range c.preReqs { + if other == preReq || preReq.isPreReq(other) { + return true + } + } + return false +} + +// After declares that the call may only match after preReq has been exhausted. +func (c *Call) After(preReq *Call) *Call { + c.t.Helper() + + if c == preReq { + c.t.Fatalf("A call isn't allowed to be its own prerequisite") + } + if preReq.isPreReq(c) { + c.t.Fatalf("Loop in call order: %v is a prerequisite to %v (possibly indirectly).", c, preReq) + } + + c.preReqs = append(c.preReqs, preReq) + return c +} + +// Returns true if the minimum number of calls have been made. +func (c *Call) satisfied() bool { + return c.numCalls >= c.minCalls +} + +// Returns true if the maximum number of calls have been made. +func (c *Call) exhausted() bool { + return c.numCalls >= c.maxCalls +} + +func (c *Call) String() string { + args := make([]string, len(c.args)) + for i, arg := range c.args { + args[i] = arg.String() + } + arguments := strings.Join(args, ", ") + return fmt.Sprintf("%T.%v(%s) %s", c.receiver, c.method, arguments, c.origin) +} + +// Tests if the given call matches the expected call. +// If yes, returns nil. If no, returns error with message explaining why it does not match. +func (c *Call) matches(args []any) error { + if !c.methodType.IsVariadic() { + if len(args) != len(c.args) { + return fmt.Errorf("expected call at %s has the wrong number of arguments. Got: %d, want: %d", + c.origin, len(args), len(c.args)) + } + + for i, m := range c.args { + if !m.Matches(args[i]) { + return fmt.Errorf( + "expected call at %s doesn't match the argument at index %d.\nGot: %v\nWant: %v", + c.origin, i, formatGottenArg(m, args[i]), m, + ) + } + } + } else { + if len(c.args) < c.methodType.NumIn()-1 { + return fmt.Errorf("expected call at %s has the wrong number of matchers. Got: %d, want: %d", + c.origin, len(c.args), c.methodType.NumIn()-1) + } + if len(c.args) != c.methodType.NumIn() && len(args) != len(c.args) { + return fmt.Errorf("expected call at %s has the wrong number of arguments. Got: %d, want: %d", + c.origin, len(args), len(c.args)) + } + if len(args) < len(c.args)-1 { + return fmt.Errorf("expected call at %s has the wrong number of arguments. Got: %d, want: greater than or equal to %d", + c.origin, len(args), len(c.args)-1) + } + + for i, m := range c.args { + if i < c.methodType.NumIn()-1 { + // Non-variadic args + if !m.Matches(args[i]) { + return fmt.Errorf("expected call at %s doesn't match the argument at index %s.\nGot: %v\nWant: %v", + c.origin, strconv.Itoa(i), formatGottenArg(m, args[i]), m) + } + continue + } + // The last arg has a possibility of a variadic argument, so let it branch + + // sample: Foo(a int, b int, c ...int) + if i < len(c.args) && i < len(args) { + if m.Matches(args[i]) { + // Got Foo(a, b, c) want Foo(matcherA, matcherB, gomock.Any()) + // Got Foo(a, b, c) want Foo(matcherA, matcherB, someSliceMatcher) + // Got Foo(a, b, c) want Foo(matcherA, matcherB, matcherC) + // Got Foo(a, b) want Foo(matcherA, matcherB) + // Got Foo(a, b, c, d) want Foo(matcherA, matcherB, matcherC, matcherD) + continue + } + } + + // The number of actual args don't match the number of matchers, + // or the last matcher is a slice and the last arg is not. + // If this function still matches it is because the last matcher + // matches all the remaining arguments or the lack of any. + // Convert the remaining arguments, if any, into a slice of the + // expected type. + vArgsType := c.methodType.In(c.methodType.NumIn() - 1) + vArgs := reflect.MakeSlice(vArgsType, 0, len(args)-i) + for _, arg := range args[i:] { + vArgs = reflect.Append(vArgs, reflect.ValueOf(arg)) + } + if m.Matches(vArgs.Interface()) { + // Got Foo(a, b, c, d, e) want Foo(matcherA, matcherB, gomock.Any()) + // Got Foo(a, b, c, d, e) want Foo(matcherA, matcherB, someSliceMatcher) + // Got Foo(a, b) want Foo(matcherA, matcherB, gomock.Any()) + // Got Foo(a, b) want Foo(matcherA, matcherB, someEmptySliceMatcher) + break + } + // Wrong number of matchers or not match. Fail. + // Got Foo(a, b) want Foo(matcherA, matcherB, matcherC, matcherD) + // Got Foo(a, b, c) want Foo(matcherA, matcherB, matcherC, matcherD) + // Got Foo(a, b, c, d) want Foo(matcherA, matcherB, matcherC, matcherD, matcherE) + // Got Foo(a, b, c, d, e) want Foo(matcherA, matcherB, matcherC, matcherD) + // Got Foo(a, b, c) want Foo(matcherA, matcherB) + + return fmt.Errorf("expected call at %s doesn't match the argument at index %s.\nGot: %v\nWant: %v", + c.origin, strconv.Itoa(i), formatGottenArg(m, args[i:]), c.args[i]) + } + } + + // Check that all prerequisite calls have been satisfied. + for _, preReqCall := range c.preReqs { + if !preReqCall.satisfied() { + return fmt.Errorf("expected call at %s doesn't have a prerequisite call satisfied:\n%v\nshould be called before:\n%v", + c.origin, preReqCall, c) + } + } + + // Check that the call is not exhausted. + if c.exhausted() { + return fmt.Errorf("expected call at %s has already been called the max number of times", c.origin) + } + + return nil +} + +// dropPrereqs tells the expected Call to not re-check prerequisite calls any +// longer, and to return its current set. +func (c *Call) dropPrereqs() (preReqs []*Call) { + preReqs = c.preReqs + c.preReqs = nil + return +} + +func (c *Call) call() []func([]any) []any { + c.numCalls++ + return c.actions +} + +// InOrder declares that the given calls should occur in order. +// It panics if the type of any of the arguments isn't *Call or a generated +// mock with an embedded *Call. +func InOrder(args ...any) { + calls := make([]*Call, 0, len(args)) + for i := 0; i < len(args); i++ { + if call := getCall(args[i]); call != nil { + calls = append(calls, call) + continue + } + panic(fmt.Sprintf( + "invalid argument at position %d of type %T, InOrder expects *gomock.Call or generated mock types with an embedded *gomock.Call", + i, + args[i], + )) + } + for i := 1; i < len(calls); i++ { + calls[i].After(calls[i-1]) + } +} + +// getCall checks if the parameter is a *Call or a generated struct +// that wraps a *Call and returns the *Call pointer - if neither, it returns nil. +func getCall(arg any) *Call { + if call, ok := arg.(*Call); ok { + return call + } + t := reflect.ValueOf(arg) + if t.Kind() != reflect.Ptr && t.Kind() != reflect.Interface { + return nil + } + t = t.Elem() + for i := 0; i < t.NumField(); i++ { + f := t.Field(i) + if !f.CanInterface() { + continue + } + if call, ok := f.Interface().(*Call); ok { + return call + } + } + return nil +} + +func setSlice(arg any, v reflect.Value) { + va := reflect.ValueOf(arg) + for i := 0; i < v.Len(); i++ { + va.Index(i).Set(v.Index(i)) + } +} + +func setMap(arg any, v reflect.Value) { + va := reflect.ValueOf(arg) + for _, e := range va.MapKeys() { + va.SetMapIndex(e, reflect.Value{}) + } + for _, e := range v.MapKeys() { + va.SetMapIndex(e, v.MapIndex(e)) + } +} + +func (c *Call) addAction(action func([]any) []any) { + c.actions = append(c.actions, action) +} + +func formatGottenArg(m Matcher, arg any) string { + got := fmt.Sprintf("%v (%T)", arg, arg) + if gs, ok := m.(GotFormatter); ok { + got = gs.Got(arg) + } + return got +} diff --git a/vendor/go.uber.org/mock/gomock/callset.go b/vendor/go.uber.org/mock/gomock/callset.go new file mode 100644 index 00000000..f5cc592d --- /dev/null +++ b/vendor/go.uber.org/mock/gomock/callset.go @@ -0,0 +1,164 @@ +// Copyright 2011 Google Inc. +// +// 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. + +package gomock + +import ( + "bytes" + "errors" + "fmt" + "sync" +) + +// callSet represents a set of expected calls, indexed by receiver and method +// name. +type callSet struct { + // Calls that are still expected. + expected map[callSetKey][]*Call + expectedMu *sync.Mutex + // Calls that have been exhausted. + exhausted map[callSetKey][]*Call + // when set to true, existing call expectations are overridden when new call expectations are made + allowOverride bool +} + +// callSetKey is the key in the maps in callSet +type callSetKey struct { + receiver any + fname string +} + +func newCallSet() *callSet { + return &callSet{ + expected: make(map[callSetKey][]*Call), + expectedMu: &sync.Mutex{}, + exhausted: make(map[callSetKey][]*Call), + } +} + +func newOverridableCallSet() *callSet { + return &callSet{ + expected: make(map[callSetKey][]*Call), + expectedMu: &sync.Mutex{}, + exhausted: make(map[callSetKey][]*Call), + allowOverride: true, + } +} + +// Add adds a new expected call. +func (cs callSet) Add(call *Call) { + key := callSetKey{call.receiver, call.method} + + cs.expectedMu.Lock() + defer cs.expectedMu.Unlock() + + m := cs.expected + if call.exhausted() { + m = cs.exhausted + } + if cs.allowOverride { + m[key] = make([]*Call, 0) + } + + m[key] = append(m[key], call) +} + +// Remove removes an expected call. +func (cs callSet) Remove(call *Call) { + key := callSetKey{call.receiver, call.method} + + cs.expectedMu.Lock() + defer cs.expectedMu.Unlock() + + calls := cs.expected[key] + for i, c := range calls { + if c == call { + // maintain order for remaining calls + cs.expected[key] = append(calls[:i], calls[i+1:]...) + cs.exhausted[key] = append(cs.exhausted[key], call) + break + } + } +} + +// FindMatch searches for a matching call. Returns error with explanation message if no call matched. +func (cs callSet) FindMatch(receiver any, method string, args []any) (*Call, error) { + key := callSetKey{receiver, method} + + cs.expectedMu.Lock() + defer cs.expectedMu.Unlock() + + // Search through the expected calls. + expected := cs.expected[key] + var callsErrors bytes.Buffer + for _, call := range expected { + err := call.matches(args) + if err != nil { + _, _ = fmt.Fprintf(&callsErrors, "\n%v", err) + } else { + return call, nil + } + } + + // If we haven't found a match then search through the exhausted calls so we + // get useful error messages. + exhausted := cs.exhausted[key] + for _, call := range exhausted { + if err := call.matches(args); err != nil { + _, _ = fmt.Fprintf(&callsErrors, "\n%v", err) + continue + } + _, _ = fmt.Fprintf( + &callsErrors, "all expected calls for method %q have been exhausted", method, + ) + } + + if len(expected)+len(exhausted) == 0 { + _, _ = fmt.Fprintf(&callsErrors, "there are no expected calls of the method %q for that receiver", method) + } + + return nil, errors.New(callsErrors.String()) +} + +// Failures returns the calls that are not satisfied. +func (cs callSet) Failures() []*Call { + cs.expectedMu.Lock() + defer cs.expectedMu.Unlock() + + failures := make([]*Call, 0, len(cs.expected)) + for _, calls := range cs.expected { + for _, call := range calls { + if !call.satisfied() { + failures = append(failures, call) + } + } + } + return failures +} + +// Satisfied returns true in case all expected calls in this callSet are satisfied. +func (cs callSet) Satisfied() bool { + cs.expectedMu.Lock() + defer cs.expectedMu.Unlock() + + for _, calls := range cs.expected { + for _, call := range calls { + if !call.satisfied() { + return false + } + } + } + + return true +} diff --git a/vendor/go.uber.org/mock/gomock/controller.go b/vendor/go.uber.org/mock/gomock/controller.go new file mode 100644 index 00000000..9d17a2f0 --- /dev/null +++ b/vendor/go.uber.org/mock/gomock/controller.go @@ -0,0 +1,318 @@ +// Copyright 2010 Google Inc. +// +// 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. + +package gomock + +import ( + "context" + "fmt" + "reflect" + "runtime" + "sync" +) + +// A TestReporter is something that can be used to report test failures. It +// is satisfied by the standard library's *testing.T. +type TestReporter interface { + Errorf(format string, args ...any) + Fatalf(format string, args ...any) +} + +// TestHelper is a TestReporter that has the Helper method. It is satisfied +// by the standard library's *testing.T. +type TestHelper interface { + TestReporter + Helper() +} + +// cleanuper is used to check if TestHelper also has the `Cleanup` method. A +// common pattern is to pass in a `*testing.T` to +// `NewController(t TestReporter)`. In Go 1.14+, `*testing.T` has a cleanup +// method. This can be utilized to call `Finish()` so the caller of this library +// does not have to. +type cleanuper interface { + Cleanup(func()) +} + +// A Controller represents the top-level control of a mock ecosystem. It +// defines the scope and lifetime of mock objects, as well as their +// expectations. It is safe to call Controller's methods from multiple +// goroutines. Each test should create a new Controller and invoke Finish via +// defer. +// +// func TestFoo(t *testing.T) { +// ctrl := gomock.NewController(t) +// // .. +// } +// +// func TestBar(t *testing.T) { +// t.Run("Sub-Test-1", st) { +// ctrl := gomock.NewController(st) +// // .. +// }) +// t.Run("Sub-Test-2", st) { +// ctrl := gomock.NewController(st) +// // .. +// }) +// }) +type Controller struct { + // T should only be called within a generated mock. It is not intended to + // be used in user code and may be changed in future versions. T is the + // TestReporter passed in when creating the Controller via NewController. + // If the TestReporter does not implement a TestHelper it will be wrapped + // with a nopTestHelper. + T TestHelper + mu sync.Mutex + expectedCalls *callSet + finished bool +} + +// NewController returns a new Controller. It is the preferred way to create a Controller. +// +// Passing [*testing.T] registers cleanup function to automatically call [Controller.Finish] +// when the test and all its subtests complete. +func NewController(t TestReporter, opts ...ControllerOption) *Controller { + h, ok := t.(TestHelper) + if !ok { + h = &nopTestHelper{t} + } + ctrl := &Controller{ + T: h, + expectedCalls: newCallSet(), + } + for _, opt := range opts { + opt.apply(ctrl) + } + if c, ok := isCleanuper(ctrl.T); ok { + c.Cleanup(func() { + ctrl.T.Helper() + ctrl.finish(true, nil) + }) + } + + return ctrl +} + +// ControllerOption configures how a Controller should behave. +type ControllerOption interface { + apply(*Controller) +} + +type overridableExpectationsOption struct{} + +// WithOverridableExpectations allows for overridable call expectations +// i.e., subsequent call expectations override existing call expectations +func WithOverridableExpectations() overridableExpectationsOption { + return overridableExpectationsOption{} +} + +func (o overridableExpectationsOption) apply(ctrl *Controller) { + ctrl.expectedCalls = newOverridableCallSet() +} + +type cancelReporter struct { + t TestHelper + cancel func() +} + +func (r *cancelReporter) Errorf(format string, args ...any) { + r.t.Errorf(format, args...) +} +func (r *cancelReporter) Fatalf(format string, args ...any) { + defer r.cancel() + r.t.Fatalf(format, args...) +} + +func (r *cancelReporter) Helper() { + r.t.Helper() +} + +// WithContext returns a new Controller and a Context, which is cancelled on any +// fatal failure. +func WithContext(ctx context.Context, t TestReporter) (*Controller, context.Context) { + h, ok := t.(TestHelper) + if !ok { + h = &nopTestHelper{t: t} + } + + ctx, cancel := context.WithCancel(ctx) + return NewController(&cancelReporter{t: h, cancel: cancel}), ctx +} + +type nopTestHelper struct { + t TestReporter +} + +func (h *nopTestHelper) Errorf(format string, args ...any) { + h.t.Errorf(format, args...) +} +func (h *nopTestHelper) Fatalf(format string, args ...any) { + h.t.Fatalf(format, args...) +} + +func (h nopTestHelper) Helper() {} + +// RecordCall is called by a mock. It should not be called by user code. +func (ctrl *Controller) RecordCall(receiver any, method string, args ...any) *Call { + ctrl.T.Helper() + + recv := reflect.ValueOf(receiver) + for i := 0; i < recv.Type().NumMethod(); i++ { + if recv.Type().Method(i).Name == method { + return ctrl.RecordCallWithMethodType(receiver, method, recv.Method(i).Type(), args...) + } + } + ctrl.T.Fatalf("gomock: failed finding method %s on %T", method, receiver) + panic("unreachable") +} + +// RecordCallWithMethodType is called by a mock. It should not be called by user code. +func (ctrl *Controller) RecordCallWithMethodType(receiver any, method string, methodType reflect.Type, args ...any) *Call { + ctrl.T.Helper() + + call := newCall(ctrl.T, receiver, method, methodType, args...) + + ctrl.mu.Lock() + defer ctrl.mu.Unlock() + ctrl.expectedCalls.Add(call) + + return call +} + +// Call is called by a mock. It should not be called by user code. +func (ctrl *Controller) Call(receiver any, method string, args ...any) []any { + ctrl.T.Helper() + + // Nest this code so we can use defer to make sure the lock is released. + actions := func() []func([]any) []any { + ctrl.T.Helper() + ctrl.mu.Lock() + defer ctrl.mu.Unlock() + + expected, err := ctrl.expectedCalls.FindMatch(receiver, method, args) + if err != nil { + // callerInfo's skip should be updated if the number of calls between the user's test + // and this line changes, i.e. this code is wrapped in another anonymous function. + // 0 is us, 1 is controller.Call(), 2 is the generated mock, and 3 is the user's test. + origin := callerInfo(3) + ctrl.T.Fatalf("Unexpected call to %T.%v(%v) at %s because: %s", receiver, method, args, origin, err) + } + + // Two things happen here: + // * the matching call no longer needs to check prerequite calls, + // * and the prerequite calls are no longer expected, so remove them. + preReqCalls := expected.dropPrereqs() + for _, preReqCall := range preReqCalls { + ctrl.expectedCalls.Remove(preReqCall) + } + + actions := expected.call() + if expected.exhausted() { + ctrl.expectedCalls.Remove(expected) + } + return actions + }() + + var rets []any + for _, action := range actions { + if r := action(args); r != nil { + rets = r + } + } + + return rets +} + +// Finish checks to see if all the methods that were expected to be called were called. +// It is not idempotent and therefore can only be invoked once. +func (ctrl *Controller) Finish() { + // If we're currently panicking, probably because this is a deferred call. + // This must be recovered in the deferred function. + err := recover() + ctrl.finish(false, err) +} + +// Satisfied returns whether all expected calls bound to this Controller have been satisfied. +// Calling Finish is then guaranteed to not fail due to missing calls. +func (ctrl *Controller) Satisfied() bool { + ctrl.mu.Lock() + defer ctrl.mu.Unlock() + return ctrl.expectedCalls.Satisfied() +} + +func (ctrl *Controller) finish(cleanup bool, panicErr any) { + ctrl.T.Helper() + + ctrl.mu.Lock() + defer ctrl.mu.Unlock() + + if ctrl.finished { + if _, ok := isCleanuper(ctrl.T); !ok { + ctrl.T.Fatalf("Controller.Finish was called more than once. It has to be called exactly once.") + } + return + } + ctrl.finished = true + + // Short-circuit, pass through the panic. + if panicErr != nil { + panic(panicErr) + } + + // Check that all remaining expected calls are satisfied. + failures := ctrl.expectedCalls.Failures() + for _, call := range failures { + ctrl.T.Errorf("missing call(s) to %v", call) + } + if len(failures) != 0 { + if !cleanup { + ctrl.T.Fatalf("aborting test due to missing call(s)") + return + } + ctrl.T.Errorf("aborting test due to missing call(s)") + } +} + +// callerInfo returns the file:line of the call site. skip is the number +// of stack frames to skip when reporting. 0 is callerInfo's call site. +func callerInfo(skip int) string { + if _, file, line, ok := runtime.Caller(skip + 1); ok { + return fmt.Sprintf("%s:%d", file, line) + } + return "unknown file" +} + +// isCleanuper checks it if t's base TestReporter has a Cleanup method. +func isCleanuper(t TestReporter) (cleanuper, bool) { + tr := unwrapTestReporter(t) + c, ok := tr.(cleanuper) + return c, ok +} + +// unwrapTestReporter unwraps TestReporter to the base implementation. +func unwrapTestReporter(t TestReporter) TestReporter { + tr := t + switch nt := t.(type) { + case *cancelReporter: + tr = nt.t + if h, check := tr.(*nopTestHelper); check { + tr = h.t + } + case *nopTestHelper: + tr = nt.t + default: + // not wrapped + } + return tr +} diff --git a/vendor/go.uber.org/mock/gomock/doc.go b/vendor/go.uber.org/mock/gomock/doc.go new file mode 100644 index 00000000..696dda38 --- /dev/null +++ b/vendor/go.uber.org/mock/gomock/doc.go @@ -0,0 +1,60 @@ +// Copyright 2022 Google LLC +// +// 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. + +// Package gomock is a mock framework for Go. +// +// Standard usage: +// +// (1) Define an interface that you wish to mock. +// type MyInterface interface { +// SomeMethod(x int64, y string) +// } +// (2) Use mockgen to generate a mock from the interface. +// (3) Use the mock in a test: +// func TestMyThing(t *testing.T) { +// mockCtrl := gomock.NewController(t) +// mockObj := something.NewMockMyInterface(mockCtrl) +// mockObj.EXPECT().SomeMethod(4, "blah") +// // pass mockObj to a real object and play with it. +// } +// +// By default, expected calls are not enforced to run in any particular order. +// Call order dependency can be enforced by use of InOrder and/or Call.After. +// Call.After can create more varied call order dependencies, but InOrder is +// often more convenient. +// +// The following examples create equivalent call order dependencies. +// +// Example of using Call.After to chain expected call order: +// +// firstCall := mockObj.EXPECT().SomeMethod(1, "first") +// secondCall := mockObj.EXPECT().SomeMethod(2, "second").After(firstCall) +// mockObj.EXPECT().SomeMethod(3, "third").After(secondCall) +// +// Example of using InOrder to declare expected call order: +// +// gomock.InOrder( +// mockObj.EXPECT().SomeMethod(1, "first"), +// mockObj.EXPECT().SomeMethod(2, "second"), +// mockObj.EXPECT().SomeMethod(3, "third"), +// ) +// +// The standard TestReporter most users will pass to `NewController` is a +// `*testing.T` from the context of the test. Note that this will use the +// standard `t.Error` and `t.Fatal` methods to report what happened in the test. +// In some cases this can leave your testing package in a weird state if global +// state is used since `t.Fatal` is like calling panic in the middle of a +// function. In these cases it is recommended that you pass in your own +// `TestReporter`. +package gomock diff --git a/vendor/go.uber.org/mock/gomock/matchers.go b/vendor/go.uber.org/mock/gomock/matchers.go new file mode 100644 index 00000000..c1725509 --- /dev/null +++ b/vendor/go.uber.org/mock/gomock/matchers.go @@ -0,0 +1,443 @@ +// Copyright 2010 Google Inc. +// +// 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. + +package gomock + +import ( + "fmt" + "reflect" + "regexp" + "strings" +) + +// A Matcher is a representation of a class of values. +// It is used to represent the valid or expected arguments to a mocked method. +type Matcher interface { + // Matches returns whether x is a match. + Matches(x any) bool + + // String describes what the matcher matches. + String() string +} + +// WantFormatter modifies the given Matcher's String() method to the given +// Stringer. This allows for control on how the "Want" is formatted when +// printing . +func WantFormatter(s fmt.Stringer, m Matcher) Matcher { + type matcher interface { + Matches(x any) bool + } + + return struct { + matcher + fmt.Stringer + }{ + matcher: m, + Stringer: s, + } +} + +// StringerFunc type is an adapter to allow the use of ordinary functions as +// a Stringer. If f is a function with the appropriate signature, +// StringerFunc(f) is a Stringer that calls f. +type StringerFunc func() string + +// String implements fmt.Stringer. +func (f StringerFunc) String() string { + return f() +} + +// GotFormatter is used to better print failure messages. If a matcher +// implements GotFormatter, it will use the result from Got when printing +// the failure message. +type GotFormatter interface { + // Got is invoked with the received value. The result is used when + // printing the failure message. + Got(got any) string +} + +// GotFormatterFunc type is an adapter to allow the use of ordinary +// functions as a GotFormatter. If f is a function with the appropriate +// signature, GotFormatterFunc(f) is a GotFormatter that calls f. +type GotFormatterFunc func(got any) string + +// Got implements GotFormatter. +func (f GotFormatterFunc) Got(got any) string { + return f(got) +} + +// GotFormatterAdapter attaches a GotFormatter to a Matcher. +func GotFormatterAdapter(s GotFormatter, m Matcher) Matcher { + return struct { + GotFormatter + Matcher + }{ + GotFormatter: s, + Matcher: m, + } +} + +type anyMatcher struct{} + +func (anyMatcher) Matches(any) bool { + return true +} + +func (anyMatcher) String() string { + return "is anything" +} + +type condMatcher struct { + fn func(x any) bool +} + +func (c condMatcher) Matches(x any) bool { + return c.fn(x) +} + +func (condMatcher) String() string { + return "adheres to a custom condition" +} + +type eqMatcher struct { + x any +} + +func (e eqMatcher) Matches(x any) bool { + // In case, some value is nil + if e.x == nil || x == nil { + return reflect.DeepEqual(e.x, x) + } + + // Check if types assignable and convert them to common type + x1Val := reflect.ValueOf(e.x) + x2Val := reflect.ValueOf(x) + + if x1Val.Type().AssignableTo(x2Val.Type()) { + x1ValConverted := x1Val.Convert(x2Val.Type()) + return reflect.DeepEqual(x1ValConverted.Interface(), x2Val.Interface()) + } + + return false +} + +func (e eqMatcher) String() string { + return fmt.Sprintf("is equal to %v (%T)", e.x, e.x) +} + +type nilMatcher struct{} + +func (nilMatcher) Matches(x any) bool { + if x == nil { + return true + } + + v := reflect.ValueOf(x) + switch v.Kind() { + case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, + reflect.Ptr, reflect.Slice: + return v.IsNil() + } + + return false +} + +func (nilMatcher) String() string { + return "is nil" +} + +type notMatcher struct { + m Matcher +} + +func (n notMatcher) Matches(x any) bool { + return !n.m.Matches(x) +} + +func (n notMatcher) String() string { + return "not(" + n.m.String() + ")" +} + +type regexMatcher struct { + regex *regexp.Regexp +} + +func (m regexMatcher) Matches(x any) bool { + switch t := x.(type) { + case string: + return m.regex.MatchString(t) + case []byte: + return m.regex.Match(t) + default: + return false + } +} + +func (m regexMatcher) String() string { + return "matches regex " + m.regex.String() +} + +type assignableToTypeOfMatcher struct { + targetType reflect.Type +} + +func (m assignableToTypeOfMatcher) Matches(x any) bool { + return reflect.TypeOf(x).AssignableTo(m.targetType) +} + +func (m assignableToTypeOfMatcher) String() string { + return "is assignable to " + m.targetType.Name() +} + +type anyOfMatcher struct { + matchers []Matcher +} + +func (am anyOfMatcher) Matches(x any) bool { + for _, m := range am.matchers { + if m.Matches(x) { + return true + } + } + return false +} + +func (am anyOfMatcher) String() string { + ss := make([]string, 0, len(am.matchers)) + for _, matcher := range am.matchers { + ss = append(ss, matcher.String()) + } + return strings.Join(ss, " | ") +} + +type allMatcher struct { + matchers []Matcher +} + +func (am allMatcher) Matches(x any) bool { + for _, m := range am.matchers { + if !m.Matches(x) { + return false + } + } + return true +} + +func (am allMatcher) String() string { + ss := make([]string, 0, len(am.matchers)) + for _, matcher := range am.matchers { + ss = append(ss, matcher.String()) + } + return strings.Join(ss, "; ") +} + +type lenMatcher struct { + i int +} + +func (m lenMatcher) Matches(x any) bool { + v := reflect.ValueOf(x) + switch v.Kind() { + case reflect.Array, reflect.Chan, reflect.Map, reflect.Slice, reflect.String: + return v.Len() == m.i + default: + return false + } +} + +func (m lenMatcher) String() string { + return fmt.Sprintf("has length %d", m.i) +} + +type inAnyOrderMatcher struct { + x any +} + +func (m inAnyOrderMatcher) Matches(x any) bool { + given, ok := m.prepareValue(x) + if !ok { + return false + } + wanted, ok := m.prepareValue(m.x) + if !ok { + return false + } + + if given.Len() != wanted.Len() { + return false + } + + usedFromGiven := make([]bool, given.Len()) + foundFromWanted := make([]bool, wanted.Len()) + for i := 0; i < wanted.Len(); i++ { + wantedMatcher := Eq(wanted.Index(i).Interface()) + for j := 0; j < given.Len(); j++ { + if usedFromGiven[j] { + continue + } + if wantedMatcher.Matches(given.Index(j).Interface()) { + foundFromWanted[i] = true + usedFromGiven[j] = true + break + } + } + } + + missingFromWanted := 0 + for _, found := range foundFromWanted { + if !found { + missingFromWanted++ + } + } + extraInGiven := 0 + for _, used := range usedFromGiven { + if !used { + extraInGiven++ + } + } + + return extraInGiven == 0 && missingFromWanted == 0 +} + +func (m inAnyOrderMatcher) prepareValue(x any) (reflect.Value, bool) { + xValue := reflect.ValueOf(x) + switch xValue.Kind() { + case reflect.Slice, reflect.Array: + return xValue, true + default: + return reflect.Value{}, false + } +} + +func (m inAnyOrderMatcher) String() string { + return fmt.Sprintf("has the same elements as %v", m.x) +} + +// Constructors + +// All returns a composite Matcher that returns true if and only all of the +// matchers return true. +func All(ms ...Matcher) Matcher { return allMatcher{ms} } + +// Any returns a matcher that always matches. +func Any() Matcher { return anyMatcher{} } + +// Cond returns a matcher that matches when the given function returns true +// after passing it the parameter to the mock function. +// This is particularly useful in case you want to match over a field of a custom struct, or dynamic logic. +// +// Example usage: +// +// Cond(func(x any){return x.(int) == 1}).Matches(1) // returns true +// Cond(func(x any){return x.(int) == 2}).Matches(1) // returns false +func Cond(fn func(x any) bool) Matcher { return condMatcher{fn} } + +// AnyOf returns a composite Matcher that returns true if at least one of the +// matchers returns true. +// +// Example usage: +// +// AnyOf(1, 2, 3).Matches(2) // returns true +// AnyOf(1, 2, 3).Matches(10) // returns false +// AnyOf(Nil(), Len(2)).Matches(nil) // returns true +// AnyOf(Nil(), Len(2)).Matches("hi") // returns true +// AnyOf(Nil(), Len(2)).Matches("hello") // returns false +func AnyOf(xs ...any) Matcher { + ms := make([]Matcher, 0, len(xs)) + for _, x := range xs { + if m, ok := x.(Matcher); ok { + ms = append(ms, m) + } else { + ms = append(ms, Eq(x)) + } + } + return anyOfMatcher{ms} +} + +// Eq returns a matcher that matches on equality. +// +// Example usage: +// +// Eq(5).Matches(5) // returns true +// Eq(5).Matches(4) // returns false +func Eq(x any) Matcher { return eqMatcher{x} } + +// Len returns a matcher that matches on length. This matcher returns false if +// is compared to a type that is not an array, chan, map, slice, or string. +func Len(i int) Matcher { + return lenMatcher{i} +} + +// Nil returns a matcher that matches if the received value is nil. +// +// Example usage: +// +// var x *bytes.Buffer +// Nil().Matches(x) // returns true +// x = &bytes.Buffer{} +// Nil().Matches(x) // returns false +func Nil() Matcher { return nilMatcher{} } + +// Not reverses the results of its given child matcher. +// +// Example usage: +// +// Not(Eq(5)).Matches(4) // returns true +// Not(Eq(5)).Matches(5) // returns false +func Not(x any) Matcher { + if m, ok := x.(Matcher); ok { + return notMatcher{m} + } + return notMatcher{Eq(x)} +} + +// Regex checks whether parameter matches the associated regex. +// +// Example usage: +// +// Regex("[0-9]{2}:[0-9]{2}").Matches("23:02") // returns true +// Regex("[0-9]{2}:[0-9]{2}").Matches([]byte{'2', '3', ':', '0', '2'}) // returns true +// Regex("[0-9]{2}:[0-9]{2}").Matches("hello world") // returns false +// Regex("[0-9]{2}").Matches(21) // returns false as it's not a valid type +func Regex(regexStr string) Matcher { + return regexMatcher{regex: regexp.MustCompile(regexStr)} +} + +// AssignableToTypeOf is a Matcher that matches if the parameter to the mock +// function is assignable to the type of the parameter to this function. +// +// Example usage: +// +// var s fmt.Stringer = &bytes.Buffer{} +// AssignableToTypeOf(s).Matches(time.Second) // returns true +// AssignableToTypeOf(s).Matches(99) // returns false +// +// var ctx = reflect.TypeOf((*context.Context)(nil)).Elem() +// AssignableToTypeOf(ctx).Matches(context.Background()) // returns true +func AssignableToTypeOf(x any) Matcher { + if xt, ok := x.(reflect.Type); ok { + return assignableToTypeOfMatcher{xt} + } + return assignableToTypeOfMatcher{reflect.TypeOf(x)} +} + +// InAnyOrder is a Matcher that returns true for collections of the same elements ignoring the order. +// +// Example usage: +// +// InAnyOrder([]int{1, 2, 3}).Matches([]int{1, 3, 2}) // returns true +// InAnyOrder([]int{1, 2, 3}).Matches([]int{1, 2}) // returns false +func InAnyOrder(x any) Matcher { + return inAnyOrderMatcher{x} +} diff --git a/vendor/go.uber.org/mock/mockgen/generic_go118.go b/vendor/go.uber.org/mock/mockgen/generic_go118.go new file mode 100644 index 00000000..b7b44947 --- /dev/null +++ b/vendor/go.uber.org/mock/mockgen/generic_go118.go @@ -0,0 +1,130 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build go1.18 +// +build go1.18 + +package main + +import ( + "errors" + "fmt" + "go/ast" + "go/token" + "strings" + + "go.uber.org/mock/mockgen/model" +) + +func getTypeSpecTypeParams(ts *ast.TypeSpec) []*ast.Field { + if ts == nil || ts.TypeParams == nil { + return nil + } + return ts.TypeParams.List +} + +func (p *fileParser) parseGenericType(pkg string, typ ast.Expr, tps map[string]model.Type) (model.Type, error) { + switch v := typ.(type) { + case *ast.IndexExpr: + m, err := p.parseType(pkg, v.X, tps) + if err != nil { + return nil, err + } + nm, ok := m.(*model.NamedType) + if !ok { + return m, nil + } + t, err := p.parseType(pkg, v.Index, tps) + if err != nil { + return nil, err + } + nm.TypeParams = &model.TypeParametersType{TypeParameters: []model.Type{t}} + return m, nil + case *ast.IndexListExpr: + m, err := p.parseType(pkg, v.X, tps) + if err != nil { + return nil, err + } + nm, ok := m.(*model.NamedType) + if !ok { + return m, nil + } + var ts []model.Type + for _, expr := range v.Indices { + t, err := p.parseType(pkg, expr, tps) + if err != nil { + return nil, err + } + ts = append(ts, t) + } + nm.TypeParams = &model.TypeParametersType{TypeParameters: ts} + return m, nil + } + return nil, nil +} + +func getIdentTypeParams(decl any) string { + if decl == nil { + return "" + } + ts, ok := decl.(*ast.TypeSpec) + if !ok { + return "" + } + if ts.TypeParams == nil || len(ts.TypeParams.List) == 0 { + return "" + } + var sb strings.Builder + sb.WriteString("[") + for i, v := range ts.TypeParams.List { + if i != 0 { + sb.WriteString(", ") + } + sb.WriteString(v.Names[0].Name) + } + sb.WriteString("]") + return sb.String() +} + +func (p *fileParser) parseGenericMethod(field *ast.Field, it *namedInterface, iface *model.Interface, pkg string, tps map[string]model.Type) ([]*model.Method, error) { + var indices []ast.Expr + var typ ast.Expr + switch v := field.Type.(type) { + case *ast.IndexExpr: + indices = []ast.Expr{v.Index} + typ = v.X + case *ast.IndexListExpr: + indices = v.Indices + typ = v.X + case *ast.UnaryExpr: + if v.Op == token.TILDE { + return nil, errConstraintInterface + } + return nil, fmt.Errorf("~T may only appear as constraint for %T", field.Type) + case *ast.BinaryExpr: + if v.Op == token.OR { + return nil, errConstraintInterface + } + return nil, fmt.Errorf("A|B may only appear as constraint for %T", field.Type) + default: + return nil, fmt.Errorf("don't know how to mock method of type %T", field.Type) + } + + nf := &ast.Field{ + Doc: field.Comment, + Names: field.Names, + Type: typ, + Tag: field.Tag, + Comment: field.Comment, + } + + it.embeddedInstTypeParams = indices + + return p.parseMethod(nf, it, iface, pkg, tps) +} + +var errConstraintInterface = errors.New("interface contains constraints") diff --git a/vendor/go.uber.org/mock/mockgen/generic_notgo118.go b/vendor/go.uber.org/mock/mockgen/generic_notgo118.go new file mode 100644 index 00000000..8a779c8b --- /dev/null +++ b/vendor/go.uber.org/mock/mockgen/generic_notgo118.go @@ -0,0 +1,41 @@ +// Copyright 2022 Google LLC +// +// 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. + +//go:build !go1.18 +// +build !go1.18 + +package main + +import ( + "fmt" + "go/ast" + + "go.uber.org/mock/mockgen/model" +) + +func getTypeSpecTypeParams(ts *ast.TypeSpec) []*ast.Field { + return nil +} + +func (p *fileParser) parseGenericType(pkg string, typ ast.Expr, tps map[string]model.Type) (model.Type, error) { + return nil, nil +} + +func getIdentTypeParams(decl any) string { + return "" +} + +func (p *fileParser) parseGenericMethod(field *ast.Field, it *namedInterface, iface *model.Interface, pkg string, tps map[string]model.Type) ([]*model.Method, error) { + return nil, fmt.Errorf("don't know how to mock method of type %T", field.Type) +} diff --git a/vendor/go.uber.org/mock/mockgen/mockgen.go b/vendor/go.uber.org/mock/mockgen/mockgen.go new file mode 100644 index 00000000..df7d85f0 --- /dev/null +++ b/vendor/go.uber.org/mock/mockgen/mockgen.go @@ -0,0 +1,883 @@ +// Copyright 2010 Google Inc. +// +// 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. + +// MockGen generates mock implementations of Go interfaces. +package main + +// TODO: This does not support recursive embedded interfaces. +// TODO: This does not support embedding package-local interfaces in a separate file. + +import ( + "bytes" + "encoding/json" + "errors" + "flag" + "fmt" + "go/token" + "io" + "log" + "os" + "os/exec" + "path" + "path/filepath" + "runtime" + "sort" + "strconv" + "strings" + "unicode" + + "golang.org/x/mod/modfile" + toolsimports "golang.org/x/tools/imports" + + "go.uber.org/mock/mockgen/model" +) + +const ( + gomockImportPath = "go.uber.org/mock/gomock" +) + +var ( + version = "" + commit = "none" + date = "unknown" +) + +var ( + source = flag.String("source", "", "(source mode) Input Go source file; enables source mode.") + destination = flag.String("destination", "", "Output file; defaults to stdout.") + mockNames = flag.String("mock_names", "", "Comma-separated interfaceName=mockName pairs of explicit mock names to use. Mock names default to 'Mock'+ interfaceName suffix.") + packageOut = flag.String("package", "", "Package of the generated code; defaults to the package of the input with a 'mock_' prefix.") + selfPackage = flag.String("self_package", "", "The full package import path for the generated code. The purpose of this flag is to prevent import cycles in the generated code by trying to include its own package. This can happen if the mock's package is set to one of its inputs (usually the main one) and the output is stdio so mockgen cannot detect the final output package. Setting this flag will then tell mockgen which import to exclude.") + writePkgComment = flag.Bool("write_package_comment", true, "Writes package documentation comment (godoc) if true.") + writeSourceComment = flag.Bool("write_source_comment", true, "Writes original file (source mode) or interface names (reflect mode) comment if true.") + writeGenerateDirective = flag.Bool("write_generate_directive", false, "Add //go:generate directive to regenerate the mock") + copyrightFile = flag.String("copyright_file", "", "Copyright file used to add copyright header") + typed = flag.Bool("typed", false, "Generate Type-safe 'Return', 'Do', 'DoAndReturn' function") + imports = flag.String("imports", "", "(source mode) Comma-separated name=path pairs of explicit imports to use.") + auxFiles = flag.String("aux_files", "", "(source mode) Comma-separated pkg=path pairs of auxiliary Go source files.") + excludeInterfaces = flag.String("exclude_interfaces", "", "Comma-separated names of interfaces to be excluded") + + debugParser = flag.Bool("debug_parser", false, "Print out parser results only.") + showVersion = flag.Bool("version", false, "Print version.") +) + +func main() { + flag.Usage = usage + flag.Parse() + + if *showVersion { + printVersion() + return + } + + var pkg *model.Package + var err error + var packageName string + if *source != "" { + pkg, err = sourceMode(*source) + } else { + if flag.NArg() != 2 { + usage() + log.Fatal("Expected exactly two arguments") + } + packageName = flag.Arg(0) + interfaces := strings.Split(flag.Arg(1), ",") + if packageName == "." { + dir, err := os.Getwd() + if err != nil { + log.Fatalf("Get current directory failed: %v", err) + } + packageName, err = packageNameOfDir(dir) + if err != nil { + log.Fatalf("Parse package name failed: %v", err) + } + } + pkg, err = reflectMode(packageName, interfaces) + } + if err != nil { + log.Fatalf("Loading input failed: %v", err) + } + + if *debugParser { + pkg.Print(os.Stdout) + return + } + + outputPackageName := *packageOut + if outputPackageName == "" { + // pkg.Name in reflect mode is the base name of the import path, + // which might have characters that are illegal to have in package names. + outputPackageName = "mock_" + sanitize(pkg.Name) + } + + // outputPackagePath represents the fully qualified name of the package of + // the generated code. Its purposes are to prevent the module from importing + // itself and to prevent qualifying type names that come from its own + // package (i.e. if there is a type called X then we want to print "X" not + // "package.X" since "package" is this package). This can happen if the mock + // is output into an already existing package. + outputPackagePath := *selfPackage + if outputPackagePath == "" && *destination != "" { + dstPath, err := filepath.Abs(filepath.Dir(*destination)) + if err == nil { + pkgPath, err := parsePackageImport(dstPath) + if err == nil { + outputPackagePath = pkgPath + } else { + log.Println("Unable to infer -self_package from destination file path:", err) + } + } else { + log.Println("Unable to determine destination file path:", err) + } + } + + g := new(generator) + if *source != "" { + g.filename = *source + } else { + g.srcPackage = packageName + g.srcInterfaces = flag.Arg(1) + } + g.destination = *destination + + if *mockNames != "" { + g.mockNames = parseMockNames(*mockNames) + } + if *copyrightFile != "" { + header, err := os.ReadFile(*copyrightFile) + if err != nil { + log.Fatalf("Failed reading copyright file: %v", err) + } + + g.copyrightHeader = string(header) + } + if err := g.Generate(pkg, outputPackageName, outputPackagePath); err != nil { + log.Fatalf("Failed generating mock: %v", err) + } + output := g.Output() + dst := os.Stdout + if len(*destination) > 0 { + if err := os.MkdirAll(filepath.Dir(*destination), os.ModePerm); err != nil { + log.Fatalf("Unable to create directory: %v", err) + } + existing, err := os.ReadFile(*destination) + if err != nil && !errors.Is(err, os.ErrNotExist) { + log.Fatalf("Failed reading pre-exiting destination file: %v", err) + } + if len(existing) == len(output) && bytes.Equal(existing, output) { + return + } + f, err := os.Create(*destination) + if err != nil { + log.Fatalf("Failed opening destination file: %v", err) + } + defer f.Close() + dst = f + } + if _, err := dst.Write(output); err != nil { + log.Fatalf("Failed writing to destination: %v", err) + } +} + +func parseMockNames(names string) map[string]string { + mocksMap := make(map[string]string) + for _, kv := range strings.Split(names, ",") { + parts := strings.SplitN(kv, "=", 2) + if len(parts) != 2 || parts[1] == "" { + log.Fatalf("bad mock names spec: %v", kv) + } + mocksMap[parts[0]] = parts[1] + } + return mocksMap +} + +func parseExcludeInterfaces(names string) map[string]struct{} { + splitNames := strings.Split(names, ",") + namesSet := make(map[string]struct{}, len(splitNames)) + for _, name := range splitNames { + if name == "" { + continue + } + + namesSet[name] = struct{}{} + } + + if len(namesSet) == 0 { + return nil + } + + return namesSet +} + +func usage() { + _, _ = io.WriteString(os.Stderr, usageText) + flag.PrintDefaults() +} + +const usageText = `mockgen has two modes of operation: source and reflect. + +Source mode generates mock interfaces from a source file. +It is enabled by using the -source flag. Other flags that +may be useful in this mode are -imports and -aux_files. +Example: + mockgen -source=foo.go [other options] + +Reflect mode generates mock interfaces by building a program +that uses reflection to understand interfaces. It is enabled +by passing two non-flag arguments: an import path, and a +comma-separated list of symbols. +Example: + mockgen database/sql/driver Conn,Driver + +` + +type generator struct { + buf bytes.Buffer + indent string + mockNames map[string]string // may be empty + filename string // may be empty + destination string // may be empty + srcPackage, srcInterfaces string // may be empty + copyrightHeader string + + packageMap map[string]string // map from import path to package name +} + +func (g *generator) p(format string, args ...any) { + fmt.Fprintf(&g.buf, g.indent+format+"\n", args...) +} + +func (g *generator) in() { + g.indent += "\t" +} + +func (g *generator) out() { + if len(g.indent) > 0 { + g.indent = g.indent[0 : len(g.indent)-1] + } +} + +// sanitize cleans up a string to make a suitable package name. +func sanitize(s string) string { + t := "" + for _, r := range s { + if t == "" { + if unicode.IsLetter(r) || r == '_' { + t += string(r) + continue + } + } else { + if unicode.IsLetter(r) || unicode.IsDigit(r) || r == '_' { + t += string(r) + continue + } + } + t += "_" + } + if t == "_" { + t = "x" + } + return t +} + +func (g *generator) Generate(pkg *model.Package, outputPkgName string, outputPackagePath string) error { + if outputPkgName != pkg.Name && *selfPackage == "" { + // reset outputPackagePath if it's not passed in through -self_package + outputPackagePath = "" + } + + if g.copyrightHeader != "" { + lines := strings.Split(g.copyrightHeader, "\n") + for _, line := range lines { + g.p("// %s", line) + } + g.p("") + } + + g.p("// Code generated by MockGen. DO NOT EDIT.") + if *writeSourceComment { + if g.filename != "" { + g.p("// Source: %v", g.filename) + } else { + g.p("// Source: %v (interfaces: %v)", g.srcPackage, g.srcInterfaces) + } + } + g.p("//") + g.p("// Generated by this command:") + g.p("//") + // only log the name of the executable, not the full path + name := filepath.Base(os.Args[0]) + if runtime.GOOS == "windows" { + name = strings.TrimSuffix(name, ".exe") + } + g.p("//\t%v", strings.Join(append([]string{name}, os.Args[1:]...), " ")) + g.p("//") + + // Get all required imports, and generate unique names for them all. + im := pkg.Imports() + im[gomockImportPath] = true + + // Only import reflect if it's used. We only use reflect in mocked methods + // so only import if any of the mocked interfaces have methods. + for _, intf := range pkg.Interfaces { + if len(intf.Methods) > 0 { + im["reflect"] = true + break + } + } + + // Sort keys to make import alias generation predictable + sortedPaths := make([]string, len(im)) + x := 0 + for pth := range im { + sortedPaths[x] = pth + x++ + } + sort.Strings(sortedPaths) + + packagesName := createPackageMap(sortedPaths) + + definedImports := make(map[string]string, len(im)) + if *imports != "" { + for _, kv := range strings.Split(*imports, ",") { + eq := strings.Index(kv, "=") + if k, v := kv[:eq], kv[eq+1:]; k != "." { + definedImports[v] = k + } + } + } + + g.packageMap = make(map[string]string, len(im)) + localNames := make(map[string]bool, len(im)) + for _, pth := range sortedPaths { + base, ok := packagesName[pth] + if !ok { + base = sanitize(path.Base(pth)) + } + + // Local names for an imported package can usually be the basename of the import path. + // A couple of situations don't permit that, such as duplicate local names + // (e.g. importing "html/template" and "text/template"), or where the basename is + // a keyword (e.g. "foo/case") or when defining a name for that by using the -imports flag. + // try base0, base1, ... + pkgName := base + + if _, ok := definedImports[base]; ok { + pkgName = definedImports[base] + } + + i := 0 + for localNames[pkgName] || token.Lookup(pkgName).IsKeyword() || pkgName == "any" { + pkgName = base + strconv.Itoa(i) + i++ + } + + // Avoid importing package if source pkg == output pkg + if pth == pkg.PkgPath && outputPackagePath == pkg.PkgPath { + continue + } + + g.packageMap[pth] = pkgName + localNames[pkgName] = true + } + + if *writePkgComment { + // Ensure there's an empty line before the package to follow the recommendations: + // https://github.com/golang/go/wiki/CodeReviewComments#package-comments + g.p("") + + g.p("// Package %v is a generated GoMock package.", outputPkgName) + } + g.p("package %v", outputPkgName) + g.p("") + g.p("import (") + g.in() + for pkgPath, pkgName := range g.packageMap { + if pkgPath == outputPackagePath { + continue + } + g.p("%v %q", pkgName, pkgPath) + } + for _, pkgPath := range pkg.DotImports { + g.p(". %q", pkgPath) + } + g.out() + g.p(")") + + if *writeGenerateDirective { + g.p("//go:generate %v", strings.Join(os.Args, " ")) + } + + for _, intf := range pkg.Interfaces { + if err := g.GenerateMockInterface(intf, outputPackagePath); err != nil { + return err + } + } + + return nil +} + +// The name of the mock type to use for the given interface identifier. +func (g *generator) mockName(typeName string) string { + if mockName, ok := g.mockNames[typeName]; ok { + return mockName + } + + return "Mock" + typeName +} + +// formattedTypeParams returns a long and short form of type param info used for +// printing. If analyzing a interface with type param [I any, O any] the result +// will be: +// "[I any, O any]", "[I, O]" +func (g *generator) formattedTypeParams(it *model.Interface, pkgOverride string) (string, string) { + if len(it.TypeParams) == 0 { + return "", "" + } + var long, short strings.Builder + long.WriteString("[") + short.WriteString("[") + for i, v := range it.TypeParams { + if i != 0 { + long.WriteString(", ") + short.WriteString(", ") + } + long.WriteString(v.Name) + short.WriteString(v.Name) + long.WriteString(fmt.Sprintf(" %s", v.Type.String(g.packageMap, pkgOverride))) + } + long.WriteString("]") + short.WriteString("]") + return long.String(), short.String() +} + +func (g *generator) GenerateMockInterface(intf *model.Interface, outputPackagePath string) error { + mockType := g.mockName(intf.Name) + longTp, shortTp := g.formattedTypeParams(intf, outputPackagePath) + + g.p("") + g.p("// %v is a mock of %v interface.", mockType, intf.Name) + g.p("type %v%v struct {", mockType, longTp) + g.in() + g.p("ctrl *gomock.Controller") + g.p("recorder *%vMockRecorder%v", mockType, shortTp) + g.out() + g.p("}") + g.p("") + + g.p("// %vMockRecorder is the mock recorder for %v.", mockType, mockType) + g.p("type %vMockRecorder%v struct {", mockType, longTp) + g.in() + g.p("mock *%v%v", mockType, shortTp) + g.out() + g.p("}") + g.p("") + + g.p("// New%v creates a new mock instance.", mockType) + g.p("func New%v%v(ctrl *gomock.Controller) *%v%v {", mockType, longTp, mockType, shortTp) + g.in() + g.p("mock := &%v%v{ctrl: ctrl}", mockType, shortTp) + g.p("mock.recorder = &%vMockRecorder%v{mock}", mockType, shortTp) + g.p("return mock") + g.out() + g.p("}") + g.p("") + + // XXX: possible name collision here if someone has EXPECT in their interface. + g.p("// EXPECT returns an object that allows the caller to indicate expected use.") + g.p("func (m *%v%v) EXPECT() *%vMockRecorder%v {", mockType, shortTp, mockType, shortTp) + g.in() + g.p("return m.recorder") + g.out() + g.p("}") + + g.GenerateMockMethods(mockType, intf, outputPackagePath, longTp, shortTp, *typed) + + return nil +} + +type byMethodName []*model.Method + +func (b byMethodName) Len() int { return len(b) } +func (b byMethodName) Swap(i, j int) { b[i], b[j] = b[j], b[i] } +func (b byMethodName) Less(i, j int) bool { return b[i].Name < b[j].Name } + +func (g *generator) GenerateMockMethods(mockType string, intf *model.Interface, pkgOverride, longTp, shortTp string, typed bool) { + sort.Sort(byMethodName(intf.Methods)) + for _, m := range intf.Methods { + g.p("") + _ = g.GenerateMockMethod(mockType, m, pkgOverride, shortTp) + g.p("") + _ = g.GenerateMockRecorderMethod(intf, m, shortTp, typed) + if typed { + g.p("") + _ = g.GenerateMockReturnCallMethod(intf, m, pkgOverride, longTp, shortTp) + } + } +} + +func makeArgString(argNames, argTypes []string) string { + args := make([]string, len(argNames)) + for i, name := range argNames { + // specify the type only once for consecutive args of the same type + if i+1 < len(argTypes) && argTypes[i] == argTypes[i+1] { + args[i] = name + } else { + args[i] = name + " " + argTypes[i] + } + } + return strings.Join(args, ", ") +} + +// GenerateMockMethod generates a mock method implementation. +// If non-empty, pkgOverride is the package in which unqualified types reside. +func (g *generator) GenerateMockMethod(mockType string, m *model.Method, pkgOverride, shortTp string) error { + argNames := g.getArgNames(m, true /* in */) + argTypes := g.getArgTypes(m, pkgOverride, true /* in */) + argString := makeArgString(argNames, argTypes) + + rets := make([]string, len(m.Out)) + for i, p := range m.Out { + rets[i] = p.Type.String(g.packageMap, pkgOverride) + } + retString := strings.Join(rets, ", ") + if len(rets) > 1 { + retString = "(" + retString + ")" + } + if retString != "" { + retString = " " + retString + } + + ia := newIdentifierAllocator(argNames) + idRecv := ia.allocateIdentifier("m") + + g.p("// %v mocks base method.", m.Name) + g.p("func (%v *%v%v) %v(%v)%v {", idRecv, mockType, shortTp, m.Name, argString, retString) + g.in() + g.p("%s.ctrl.T.Helper()", idRecv) + + var callArgs string + if m.Variadic == nil { + if len(argNames) > 0 { + callArgs = ", " + strings.Join(argNames, ", ") + } + } else { + // Non-trivial. The generated code must build a []any, + // but the variadic argument may be any type. + idVarArgs := ia.allocateIdentifier("varargs") + idVArg := ia.allocateIdentifier("a") + g.p("%s := []any{%s}", idVarArgs, strings.Join(argNames[:len(argNames)-1], ", ")) + g.p("for _, %s := range %s {", idVArg, argNames[len(argNames)-1]) + g.in() + g.p("%s = append(%s, %s)", idVarArgs, idVarArgs, idVArg) + g.out() + g.p("}") + callArgs = ", " + idVarArgs + "..." + } + if len(m.Out) == 0 { + g.p(`%v.ctrl.Call(%v, %q%v)`, idRecv, idRecv, m.Name, callArgs) + } else { + idRet := ia.allocateIdentifier("ret") + g.p(`%v := %v.ctrl.Call(%v, %q%v)`, idRet, idRecv, idRecv, m.Name, callArgs) + + // Go does not allow "naked" type assertions on nil values, so we use the two-value form here. + // The value of that is either (x.(T), true) or (Z, false), where Z is the zero value for T. + // Happily, this coincides with the semantics we want here. + retNames := make([]string, len(rets)) + for i, t := range rets { + retNames[i] = ia.allocateIdentifier(fmt.Sprintf("ret%d", i)) + g.p("%s, _ := %s[%d].(%s)", retNames[i], idRet, i, t) + } + g.p("return " + strings.Join(retNames, ", ")) + } + + g.out() + g.p("}") + return nil +} + +func (g *generator) GenerateMockRecorderMethod(intf *model.Interface, m *model.Method, shortTp string, typed bool) error { + mockType := g.mockName(intf.Name) + argNames := g.getArgNames(m, true) + + var argString string + if m.Variadic == nil { + argString = strings.Join(argNames, ", ") + } else { + argString = strings.Join(argNames[:len(argNames)-1], ", ") + } + if argString != "" { + argString += " any" + } + + if m.Variadic != nil { + if argString != "" { + argString += ", " + } + argString += fmt.Sprintf("%s ...any", argNames[len(argNames)-1]) + } + + ia := newIdentifierAllocator(argNames) + idRecv := ia.allocateIdentifier("mr") + + g.p("// %v indicates an expected call of %v.", m.Name, m.Name) + if typed { + g.p("func (%s *%vMockRecorder%v) %v(%v) *%s%sCall%s {", idRecv, mockType, shortTp, m.Name, argString, mockType, m.Name, shortTp) + } else { + g.p("func (%s *%vMockRecorder%v) %v(%v) *gomock.Call {", idRecv, mockType, shortTp, m.Name, argString) + } + + g.in() + g.p("%s.mock.ctrl.T.Helper()", idRecv) + + var callArgs string + if m.Variadic == nil { + if len(argNames) > 0 { + callArgs = ", " + strings.Join(argNames, ", ") + } + } else { + if len(argNames) == 1 { + // Easy: just use ... to push the arguments through. + callArgs = ", " + argNames[0] + "..." + } else { + // Hard: create a temporary slice. + idVarArgs := ia.allocateIdentifier("varargs") + g.p("%s := append([]any{%s}, %s...)", + idVarArgs, + strings.Join(argNames[:len(argNames)-1], ", "), + argNames[len(argNames)-1]) + callArgs = ", " + idVarArgs + "..." + } + } + if typed { + g.p(`call := %s.mock.ctrl.RecordCallWithMethodType(%s.mock, "%s", reflect.TypeOf((*%s%s)(nil).%s)%s)`, idRecv, idRecv, m.Name, mockType, shortTp, m.Name, callArgs) + g.p(`return &%s%sCall%s{Call: call}`, mockType, m.Name, shortTp) + } else { + g.p(`return %s.mock.ctrl.RecordCallWithMethodType(%s.mock, "%s", reflect.TypeOf((*%s%s)(nil).%s)%s)`, idRecv, idRecv, m.Name, mockType, shortTp, m.Name, callArgs) + } + + g.out() + g.p("}") + return nil +} + +func (g *generator) GenerateMockReturnCallMethod(intf *model.Interface, m *model.Method, pkgOverride, longTp, shortTp string) error { + mockType := g.mockName(intf.Name) + argNames := g.getArgNames(m, true /* in */) + retNames := g.getArgNames(m, false /* out */) + argTypes := g.getArgTypes(m, pkgOverride, true /* in */) + retTypes := g.getArgTypes(m, pkgOverride, false /* out */) + argString := strings.Join(argTypes, ", ") + + rets := make([]string, len(m.Out)) + for i, p := range m.Out { + rets[i] = p.Type.String(g.packageMap, pkgOverride) + } + + var retString string + switch { + case len(rets) == 1: + retString = " " + rets[0] + case len(rets) > 1: + retString = " (" + strings.Join(rets, ", ") + ")" + } + + ia := newIdentifierAllocator(argNames) + idRecv := ia.allocateIdentifier("c") + + recvStructName := mockType + m.Name + + g.p("// %s%sCall wrap *gomock.Call", mockType, m.Name) + g.p("type %s%sCall%s struct{", mockType, m.Name, longTp) + g.in() + g.p("*gomock.Call") + g.out() + g.p("}") + + g.p("// Return rewrite *gomock.Call.Return") + g.p("func (%s *%sCall%s) Return(%v) *%sCall%s {", idRecv, recvStructName, shortTp, makeArgString(retNames, retTypes), recvStructName, shortTp) + g.in() + var retArgs string + if len(retNames) > 0 { + retArgs = strings.Join(retNames, ", ") + } + g.p(`%s.Call = %v.Call.Return(%v)`, idRecv, idRecv, retArgs) + g.p("return %s", idRecv) + g.out() + g.p("}") + + g.p("// Do rewrite *gomock.Call.Do") + g.p("func (%s *%sCall%s) Do(f func(%v)%v) *%sCall%s {", idRecv, recvStructName, shortTp, argString, retString, recvStructName, shortTp) + g.in() + g.p(`%s.Call = %v.Call.Do(f)`, idRecv, idRecv) + g.p("return %s", idRecv) + g.out() + g.p("}") + + g.p("// DoAndReturn rewrite *gomock.Call.DoAndReturn") + g.p("func (%s *%sCall%s) DoAndReturn(f func(%v)%v) *%sCall%s {", idRecv, recvStructName, shortTp, argString, retString, recvStructName, shortTp) + g.in() + g.p(`%s.Call = %v.Call.DoAndReturn(f)`, idRecv, idRecv) + g.p("return %s", idRecv) + g.out() + g.p("}") + return nil +} + +func (g *generator) getArgNames(m *model.Method, in bool) []string { + var params []*model.Parameter + if in { + params = m.In + } else { + params = m.Out + } + argNames := make([]string, len(params)) + for i, p := range params { + name := p.Name + if name == "" || name == "_" { + name = fmt.Sprintf("arg%d", i) + } + argNames[i] = name + } + if m.Variadic != nil && in { + name := m.Variadic.Name + if name == "" { + name = fmt.Sprintf("arg%d", len(params)) + } + argNames = append(argNames, name) + } + return argNames +} + +func (g *generator) getArgTypes(m *model.Method, pkgOverride string, in bool) []string { + var params []*model.Parameter + if in { + params = m.In + } else { + params = m.Out + } + argTypes := make([]string, len(params)) + for i, p := range params { + argTypes[i] = p.Type.String(g.packageMap, pkgOverride) + } + if m.Variadic != nil { + argTypes = append(argTypes, "..."+m.Variadic.Type.String(g.packageMap, pkgOverride)) + } + return argTypes +} + +type identifierAllocator map[string]struct{} + +func newIdentifierAllocator(taken []string) identifierAllocator { + a := make(identifierAllocator, len(taken)) + for _, s := range taken { + a[s] = struct{}{} + } + return a +} + +func (o identifierAllocator) allocateIdentifier(want string) string { + id := want + for i := 2; ; i++ { + if _, ok := o[id]; !ok { + o[id] = struct{}{} + return id + } + id = want + "_" + strconv.Itoa(i) + } +} + +// Output returns the generator's output, formatted in the standard Go style. +func (g *generator) Output() []byte { + src, err := toolsimports.Process(g.destination, g.buf.Bytes(), nil) + if err != nil { + log.Fatalf("Failed to format generated source code: %s\n%s", err, g.buf.String()) + } + return src +} + +// createPackageMap returns a map of import path to package name +// for specified importPaths. +func createPackageMap(importPaths []string) map[string]string { + var pkg struct { + Name string + ImportPath string + } + pkgMap := make(map[string]string) + b := bytes.NewBuffer(nil) + args := []string{"list", "-json"} + args = append(args, importPaths...) + cmd := exec.Command("go", args...) + cmd.Stdout = b + cmd.Run() + dec := json.NewDecoder(b) + for dec.More() { + err := dec.Decode(&pkg) + if err != nil { + log.Printf("failed to decode 'go list' output: %v", err) + continue + } + pkgMap[pkg.ImportPath] = pkg.Name + } + return pkgMap +} + +func printVersion() { + if version != "" { + fmt.Printf("v%s\nCommit: %s\nDate: %s\n", version, commit, date) + } else { + printModuleVersion() + } +} + +// parseImportPackage get package import path via source file +// an alternative implementation is to use: +// cfg := &packages.Config{Mode: packages.NeedName, Tests: true, Dir: srcDir} +// pkgs, err := packages.Load(cfg, "file="+source) +// However, it will call "go list" and slow down the performance +func parsePackageImport(srcDir string) (string, error) { + moduleMode := os.Getenv("GO111MODULE") + // trying to find the module + if moduleMode != "off" { + currentDir := srcDir + for { + dat, err := os.ReadFile(filepath.Join(currentDir, "go.mod")) + if os.IsNotExist(err) { + if currentDir == filepath.Dir(currentDir) { + // at the root + break + } + currentDir = filepath.Dir(currentDir) + continue + } else if err != nil { + return "", err + } + modulePath := modfile.ModulePath(dat) + return filepath.ToSlash(filepath.Join(modulePath, strings.TrimPrefix(srcDir, currentDir))), nil + } + } + // fall back to GOPATH mode + goPaths := os.Getenv("GOPATH") + if goPaths == "" { + return "", fmt.Errorf("GOPATH is not set") + } + goPathList := strings.Split(goPaths, string(os.PathListSeparator)) + for _, goPath := range goPathList { + sourceRoot := filepath.Join(goPath, "src") + string(os.PathSeparator) + if strings.HasPrefix(srcDir, sourceRoot) { + return filepath.ToSlash(strings.TrimPrefix(srcDir, sourceRoot)), nil + } + } + return "", errOutsideGoPath +} diff --git a/vendor/go.uber.org/mock/mockgen/model/model.go b/vendor/go.uber.org/mock/mockgen/model/model.go new file mode 100644 index 00000000..853dbf2d --- /dev/null +++ b/vendor/go.uber.org/mock/mockgen/model/model.go @@ -0,0 +1,533 @@ +// Copyright 2012 Google Inc. +// +// 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. + +// Package model contains the data model necessary for generating mock implementations. +package model + +import ( + "encoding/gob" + "fmt" + "io" + "reflect" + "strings" +) + +// pkgPath is the importable path for package model +const pkgPath = "go.uber.org/mock/mockgen/model" + +// Package is a Go package. It may be a subset. +type Package struct { + Name string + PkgPath string + Interfaces []*Interface + DotImports []string +} + +// Print writes the package name and its exported interfaces. +func (pkg *Package) Print(w io.Writer) { + _, _ = fmt.Fprintf(w, "package %s\n", pkg.Name) + for _, intf := range pkg.Interfaces { + intf.Print(w) + } +} + +// Imports returns the imports needed by the Package as a set of import paths. +func (pkg *Package) Imports() map[string]bool { + im := make(map[string]bool) + for _, intf := range pkg.Interfaces { + intf.addImports(im) + for _, tp := range intf.TypeParams { + tp.Type.addImports(im) + } + } + return im +} + +// Interface is a Go interface. +type Interface struct { + Name string + Methods []*Method + TypeParams []*Parameter +} + +// Print writes the interface name and its methods. +func (intf *Interface) Print(w io.Writer) { + _, _ = fmt.Fprintf(w, "interface %s\n", intf.Name) + for _, m := range intf.Methods { + m.Print(w) + } +} + +func (intf *Interface) addImports(im map[string]bool) { + for _, m := range intf.Methods { + m.addImports(im) + } +} + +// AddMethod adds a new method, de-duplicating by method name. +func (intf *Interface) AddMethod(m *Method) { + for _, me := range intf.Methods { + if me.Name == m.Name { + return + } + } + intf.Methods = append(intf.Methods, m) +} + +// Method is a single method of an interface. +type Method struct { + Name string + In, Out []*Parameter + Variadic *Parameter // may be nil +} + +// Print writes the method name and its signature. +func (m *Method) Print(w io.Writer) { + _, _ = fmt.Fprintf(w, " - method %s\n", m.Name) + if len(m.In) > 0 { + _, _ = fmt.Fprintf(w, " in:\n") + for _, p := range m.In { + p.Print(w) + } + } + if m.Variadic != nil { + _, _ = fmt.Fprintf(w, " ...:\n") + m.Variadic.Print(w) + } + if len(m.Out) > 0 { + _, _ = fmt.Fprintf(w, " out:\n") + for _, p := range m.Out { + p.Print(w) + } + } +} + +func (m *Method) addImports(im map[string]bool) { + for _, p := range m.In { + p.Type.addImports(im) + } + if m.Variadic != nil { + m.Variadic.Type.addImports(im) + } + for _, p := range m.Out { + p.Type.addImports(im) + } +} + +// Parameter is an argument or return parameter of a method. +type Parameter struct { + Name string // may be empty + Type Type +} + +// Print writes a method parameter. +func (p *Parameter) Print(w io.Writer) { + n := p.Name + if n == "" { + n = `""` + } + _, _ = fmt.Fprintf(w, " - %v: %v\n", n, p.Type.String(nil, "")) +} + +// Type is a Go type. +type Type interface { + String(pm map[string]string, pkgOverride string) string + addImports(im map[string]bool) +} + +func init() { + // Call gob.RegisterName with pkgPath as prefix to avoid conflicting with + // github.com/golang/mock/mockgen/model 's registration. + gob.RegisterName(pkgPath+".ArrayType", &ArrayType{}) + gob.RegisterName(pkgPath+".ChanType", &ChanType{}) + gob.RegisterName(pkgPath+".FuncType", &FuncType{}) + gob.RegisterName(pkgPath+".MapType", &MapType{}) + gob.RegisterName(pkgPath+".NamedType", &NamedType{}) + gob.RegisterName(pkgPath+".PointerType", &PointerType{}) + + // Call gob.RegisterName to make sure it has the consistent name registered + // for both gob decoder and encoder. + // + // For a non-pointer type, gob.Register will try to get package full path by + // calling rt.PkgPath() for a name to register. If your project has vendor + // directory, it is possible that PkgPath will get a path like this: + // ../../../vendor/go.uber.org/mock/mockgen/model + gob.RegisterName(pkgPath+".PredeclaredType", PredeclaredType("")) +} + +// ArrayType is an array or slice type. +type ArrayType struct { + Len int // -1 for slices, >= 0 for arrays + Type Type +} + +func (at *ArrayType) String(pm map[string]string, pkgOverride string) string { + s := "[]" + if at.Len > -1 { + s = fmt.Sprintf("[%d]", at.Len) + } + return s + at.Type.String(pm, pkgOverride) +} + +func (at *ArrayType) addImports(im map[string]bool) { at.Type.addImports(im) } + +// ChanType is a channel type. +type ChanType struct { + Dir ChanDir // 0, 1 or 2 + Type Type +} + +func (ct *ChanType) String(pm map[string]string, pkgOverride string) string { + s := ct.Type.String(pm, pkgOverride) + if ct.Dir == RecvDir { + return "<-chan " + s + } + if ct.Dir == SendDir { + return "chan<- " + s + } + return "chan " + s +} + +func (ct *ChanType) addImports(im map[string]bool) { ct.Type.addImports(im) } + +// ChanDir is a channel direction. +type ChanDir int + +// Constants for channel directions. +const ( + RecvDir ChanDir = 1 + SendDir ChanDir = 2 +) + +// FuncType is a function type. +type FuncType struct { + In, Out []*Parameter + Variadic *Parameter // may be nil +} + +func (ft *FuncType) String(pm map[string]string, pkgOverride string) string { + args := make([]string, len(ft.In)) + for i, p := range ft.In { + args[i] = p.Type.String(pm, pkgOverride) + } + if ft.Variadic != nil { + args = append(args, "..."+ft.Variadic.Type.String(pm, pkgOverride)) + } + rets := make([]string, len(ft.Out)) + for i, p := range ft.Out { + rets[i] = p.Type.String(pm, pkgOverride) + } + retString := strings.Join(rets, ", ") + if nOut := len(ft.Out); nOut == 1 { + retString = " " + retString + } else if nOut > 1 { + retString = " (" + retString + ")" + } + return "func(" + strings.Join(args, ", ") + ")" + retString +} + +func (ft *FuncType) addImports(im map[string]bool) { + for _, p := range ft.In { + p.Type.addImports(im) + } + if ft.Variadic != nil { + ft.Variadic.Type.addImports(im) + } + for _, p := range ft.Out { + p.Type.addImports(im) + } +} + +// MapType is a map type. +type MapType struct { + Key, Value Type +} + +func (mt *MapType) String(pm map[string]string, pkgOverride string) string { + return "map[" + mt.Key.String(pm, pkgOverride) + "]" + mt.Value.String(pm, pkgOverride) +} + +func (mt *MapType) addImports(im map[string]bool) { + mt.Key.addImports(im) + mt.Value.addImports(im) +} + +// NamedType is an exported type in a package. +type NamedType struct { + Package string // may be empty + Type string + TypeParams *TypeParametersType +} + +func (nt *NamedType) String(pm map[string]string, pkgOverride string) string { + if pkgOverride == nt.Package { + return nt.Type + nt.TypeParams.String(pm, pkgOverride) + } + prefix := pm[nt.Package] + if prefix != "" { + return prefix + "." + nt.Type + nt.TypeParams.String(pm, pkgOverride) + } + + return nt.Type + nt.TypeParams.String(pm, pkgOverride) +} + +func (nt *NamedType) addImports(im map[string]bool) { + if nt.Package != "" { + im[nt.Package] = true + } + nt.TypeParams.addImports(im) +} + +// PointerType is a pointer to another type. +type PointerType struct { + Type Type +} + +func (pt *PointerType) String(pm map[string]string, pkgOverride string) string { + return "*" + pt.Type.String(pm, pkgOverride) +} +func (pt *PointerType) addImports(im map[string]bool) { pt.Type.addImports(im) } + +// PredeclaredType is a predeclared type such as "int". +type PredeclaredType string + +func (pt PredeclaredType) String(map[string]string, string) string { return string(pt) } +func (pt PredeclaredType) addImports(map[string]bool) {} + +// TypeParametersType contains type parameters for a NamedType. +type TypeParametersType struct { + TypeParameters []Type +} + +func (tp *TypeParametersType) String(pm map[string]string, pkgOverride string) string { + if tp == nil || len(tp.TypeParameters) == 0 { + return "" + } + var sb strings.Builder + sb.WriteString("[") + for i, v := range tp.TypeParameters { + if i != 0 { + sb.WriteString(", ") + } + sb.WriteString(v.String(pm, pkgOverride)) + } + sb.WriteString("]") + return sb.String() +} + +func (tp *TypeParametersType) addImports(im map[string]bool) { + if tp == nil { + return + } + for _, v := range tp.TypeParameters { + v.addImports(im) + } +} + +// The following code is intended to be called by the program generated by ../reflect.go. + +// InterfaceFromInterfaceType returns a pointer to an interface for the +// given reflection interface type. +func InterfaceFromInterfaceType(it reflect.Type) (*Interface, error) { + if it.Kind() != reflect.Interface { + return nil, fmt.Errorf("%v is not an interface", it) + } + intf := &Interface{} + + for i := 0; i < it.NumMethod(); i++ { + mt := it.Method(i) + // TODO: need to skip unexported methods? or just raise an error? + m := &Method{ + Name: mt.Name, + } + + var err error + m.In, m.Variadic, m.Out, err = funcArgsFromType(mt.Type) + if err != nil { + return nil, err + } + + intf.AddMethod(m) + } + + return intf, nil +} + +// t's Kind must be a reflect.Func. +func funcArgsFromType(t reflect.Type) (in []*Parameter, variadic *Parameter, out []*Parameter, err error) { + nin := t.NumIn() + if t.IsVariadic() { + nin-- + } + var p *Parameter + for i := 0; i < nin; i++ { + p, err = parameterFromType(t.In(i)) + if err != nil { + return + } + in = append(in, p) + } + if t.IsVariadic() { + p, err = parameterFromType(t.In(nin).Elem()) + if err != nil { + return + } + variadic = p + } + for i := 0; i < t.NumOut(); i++ { + p, err = parameterFromType(t.Out(i)) + if err != nil { + return + } + out = append(out, p) + } + return +} + +func parameterFromType(t reflect.Type) (*Parameter, error) { + tt, err := typeFromType(t) + if err != nil { + return nil, err + } + return &Parameter{Type: tt}, nil +} + +var errorType = reflect.TypeOf((*error)(nil)).Elem() + +var byteType = reflect.TypeOf(byte(0)) + +func typeFromType(t reflect.Type) (Type, error) { + // Hack workaround for https://golang.org/issue/3853. + // This explicit check should not be necessary. + if t == byteType { + return PredeclaredType("byte"), nil + } + + if imp := t.PkgPath(); imp != "" { + return &NamedType{ + Package: impPath(imp), + Type: t.Name(), + }, nil + } + + // only unnamed or predeclared types after here + + // Lots of types have element types. Let's do the parsing and error checking for all of them. + var elemType Type + switch t.Kind() { + case reflect.Array, reflect.Chan, reflect.Map, reflect.Ptr, reflect.Slice: + var err error + elemType, err = typeFromType(t.Elem()) + if err != nil { + return nil, err + } + } + + switch t.Kind() { + case reflect.Array: + return &ArrayType{ + Len: t.Len(), + Type: elemType, + }, nil + case reflect.Bool, reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, + reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr, + reflect.Float32, reflect.Float64, reflect.Complex64, reflect.Complex128, reflect.String: + return PredeclaredType(t.Kind().String()), nil + case reflect.Chan: + var dir ChanDir + switch t.ChanDir() { + case reflect.RecvDir: + dir = RecvDir + case reflect.SendDir: + dir = SendDir + } + return &ChanType{ + Dir: dir, + Type: elemType, + }, nil + case reflect.Func: + in, variadic, out, err := funcArgsFromType(t) + if err != nil { + return nil, err + } + return &FuncType{ + In: in, + Out: out, + Variadic: variadic, + }, nil + case reflect.Interface: + // Two special interfaces. + if t.NumMethod() == 0 { + return PredeclaredType("any"), nil + } + if t == errorType { + return PredeclaredType("error"), nil + } + case reflect.Map: + kt, err := typeFromType(t.Key()) + if err != nil { + return nil, err + } + return &MapType{ + Key: kt, + Value: elemType, + }, nil + case reflect.Ptr: + return &PointerType{ + Type: elemType, + }, nil + case reflect.Slice: + return &ArrayType{ + Len: -1, + Type: elemType, + }, nil + case reflect.Struct: + if t.NumField() == 0 { + return PredeclaredType("struct{}"), nil + } + } + + // TODO: Struct, UnsafePointer + return nil, fmt.Errorf("can't yet turn %v (%v) into a model.Type", t, t.Kind()) +} + +// impPath sanitizes the package path returned by `PkgPath` method of a reflect Type so that +// it is importable. PkgPath might return a path that includes "vendor". These paths do not +// compile, so we need to remove everything up to and including "/vendor/". +// See https://github.com/golang/go/issues/12019. +func impPath(imp string) string { + if strings.HasPrefix(imp, "vendor/") { + imp = "/" + imp + } + if i := strings.LastIndex(imp, "/vendor/"); i != -1 { + imp = imp[i+len("/vendor/"):] + } + return imp +} + +// ErrorInterface represent built-in error interface. +var ErrorInterface = Interface{ + Name: "error", + Methods: []*Method{ + { + Name: "Error", + Out: []*Parameter{ + { + Name: "", + Type: PredeclaredType("string"), + }, + }, + }, + }, +} diff --git a/vendor/go.uber.org/mock/mockgen/parse.go b/vendor/go.uber.org/mock/mockgen/parse.go new file mode 100644 index 00000000..f43321c3 --- /dev/null +++ b/vendor/go.uber.org/mock/mockgen/parse.go @@ -0,0 +1,805 @@ +// Copyright 2012 Google Inc. +// +// 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. + +package main + +// This file contains the model construction by parsing source files. + +import ( + "errors" + "fmt" + "go/ast" + "go/build" + "go/importer" + "go/parser" + "go/token" + "go/types" + "log" + "os" + "path" + "path/filepath" + "strconv" + "strings" + + "go.uber.org/mock/mockgen/model" +) + +// sourceMode generates mocks via source file. +func sourceMode(source string) (*model.Package, error) { + srcDir, err := filepath.Abs(filepath.Dir(source)) + if err != nil { + return nil, fmt.Errorf("failed getting source directory: %v", err) + } + + packageImport, err := parsePackageImport(srcDir) + if err != nil { + return nil, err + } + + fs := token.NewFileSet() + file, err := parser.ParseFile(fs, source, nil, 0) + if err != nil { + return nil, fmt.Errorf("failed parsing source file %v: %v", source, err) + } + + p := &fileParser{ + fileSet: fs, + imports: make(map[string]importedPackage), + importedInterfaces: newInterfaceCache(), + auxInterfaces: newInterfaceCache(), + srcDir: srcDir, + } + + // Handle -imports. + dotImports := make(map[string]bool) + if *imports != "" { + for _, kv := range strings.Split(*imports, ",") { + eq := strings.Index(kv, "=") + k, v := kv[:eq], kv[eq+1:] + if k == "." { + dotImports[v] = true + } else { + p.imports[k] = importedPkg{path: v} + } + } + } + + if *excludeInterfaces != "" { + p.excludeNamesSet = parseExcludeInterfaces(*excludeInterfaces) + } + + // Handle -aux_files. + if err := p.parseAuxFiles(*auxFiles); err != nil { + return nil, err + } + p.addAuxInterfacesFromFile(packageImport, file) // this file + + pkg, err := p.parseFile(packageImport, file) + if err != nil { + return nil, err + } + for pkgPath := range dotImports { + pkg.DotImports = append(pkg.DotImports, pkgPath) + } + return pkg, nil +} + +type importedPackage interface { + Path() string + Parser() *fileParser +} + +type importedPkg struct { + path string + parser *fileParser +} + +func (i importedPkg) Path() string { return i.path } +func (i importedPkg) Parser() *fileParser { return i.parser } + +// duplicateImport is a bit of a misnomer. Currently the parser can't +// handle cases of multi-file packages importing different packages +// under the same name. Often these imports would not be problematic, +// so this type lets us defer raising an error unless the package name +// is actually used. +type duplicateImport struct { + name string + duplicates []string +} + +func (d duplicateImport) Error() string { + return fmt.Sprintf("%q is ambiguous because of duplicate imports: %v", d.name, d.duplicates) +} + +func (d duplicateImport) Path() string { log.Fatal(d.Error()); return "" } +func (d duplicateImport) Parser() *fileParser { log.Fatal(d.Error()); return nil } + +type interfaceCache struct { + m map[string]map[string]*namedInterface +} + +func newInterfaceCache() *interfaceCache { + return &interfaceCache{ + m: make(map[string]map[string]*namedInterface), + } +} + +func (i *interfaceCache) Set(pkg, name string, it *namedInterface) { + if _, ok := i.m[pkg]; !ok { + i.m[pkg] = make(map[string]*namedInterface) + } + i.m[pkg][name] = it +} + +func (i *interfaceCache) Get(pkg, name string) *namedInterface { + if _, ok := i.m[pkg]; !ok { + return nil + } + return i.m[pkg][name] +} + +func (i *interfaceCache) GetASTIface(pkg, name string) *ast.InterfaceType { + if _, ok := i.m[pkg]; !ok { + return nil + } + it, ok := i.m[pkg][name] + if !ok { + return nil + } + return it.it +} + +type fileParser struct { + fileSet *token.FileSet + imports map[string]importedPackage // package name => imported package + importedInterfaces *interfaceCache + auxFiles []*ast.File + auxInterfaces *interfaceCache + srcDir string + excludeNamesSet map[string]struct{} +} + +func (p *fileParser) errorf(pos token.Pos, format string, args ...any) error { + ps := p.fileSet.Position(pos) + format = "%s:%d:%d: " + format + args = append([]any{ps.Filename, ps.Line, ps.Column}, args...) + return fmt.Errorf(format, args...) +} + +func (p *fileParser) parseAuxFiles(auxFiles string) error { + auxFiles = strings.TrimSpace(auxFiles) + if auxFiles == "" { + return nil + } + for _, kv := range strings.Split(auxFiles, ",") { + parts := strings.SplitN(kv, "=", 2) + if len(parts) != 2 { + return fmt.Errorf("bad aux file spec: %v", kv) + } + pkg, fpath := parts[0], parts[1] + + file, err := parser.ParseFile(p.fileSet, fpath, nil, 0) + if err != nil { + return err + } + p.auxFiles = append(p.auxFiles, file) + p.addAuxInterfacesFromFile(pkg, file) + } + return nil +} + +func (p *fileParser) addAuxInterfacesFromFile(pkg string, file *ast.File) { + for ni := range iterInterfaces(file) { + p.auxInterfaces.Set(pkg, ni.name.Name, ni) + } +} + +// parseFile loads all file imports and auxiliary files import into the +// fileParser, parses all file interfaces and returns package model. +func (p *fileParser) parseFile(importPath string, file *ast.File) (*model.Package, error) { + allImports, dotImports := importsOfFile(file) + // Don't stomp imports provided by -imports. Those should take precedence. + for pkg, pkgI := range allImports { + if _, ok := p.imports[pkg]; !ok { + p.imports[pkg] = pkgI + } + } + // Add imports from auxiliary files, which might be needed for embedded interfaces. + // Don't stomp any other imports. + for _, f := range p.auxFiles { + auxImports, _ := importsOfFile(f) + for pkg, pkgI := range auxImports { + if _, ok := p.imports[pkg]; !ok { + p.imports[pkg] = pkgI + } + } + } + + var is []*model.Interface + for ni := range iterInterfaces(file) { + if _, ok := p.excludeNamesSet[ni.name.String()]; ok { + continue + } + i, err := p.parseInterface(ni.name.String(), importPath, ni) + if errors.Is(err, errConstraintInterface) { + continue + } + if err != nil { + return nil, err + } + is = append(is, i) + } + return &model.Package{ + Name: file.Name.String(), + PkgPath: importPath, + Interfaces: is, + DotImports: dotImports, + }, nil +} + +// parsePackage loads package specified by path, parses it and returns +// a new fileParser with the parsed imports and interfaces. +func (p *fileParser) parsePackage(path string) (*fileParser, error) { + newP := &fileParser{ + fileSet: token.NewFileSet(), + imports: make(map[string]importedPackage), + importedInterfaces: newInterfaceCache(), + auxInterfaces: newInterfaceCache(), + srcDir: p.srcDir, + } + + var pkgs map[string]*ast.Package + if imp, err := build.Import(path, newP.srcDir, build.FindOnly); err != nil { + return nil, err + } else if pkgs, err = parser.ParseDir(newP.fileSet, imp.Dir, nil, 0); err != nil { + return nil, err + } + + for _, pkg := range pkgs { + file := ast.MergePackageFiles(pkg, ast.FilterFuncDuplicates|ast.FilterUnassociatedComments|ast.FilterImportDuplicates) + for ni := range iterInterfaces(file) { + newP.importedInterfaces.Set(path, ni.name.Name, ni) + } + imports, _ := importsOfFile(file) + for pkgName, pkgI := range imports { + newP.imports[pkgName] = pkgI + } + } + return newP, nil +} + +func (p *fileParser) constructInstParams(pkg string, params []*ast.Field, instParams []model.Type, embeddedInstParams []ast.Expr, tps map[string]model.Type) ([]model.Type, error) { + pm := make(map[string]int) + var i int + for _, v := range params { + for _, n := range v.Names { + pm[n.Name] = i + instParams = append(instParams, model.PredeclaredType(n.Name)) + i++ + } + } + + var runtimeInstParams []model.Type + for _, instParam := range embeddedInstParams { + switch t := instParam.(type) { + case *ast.Ident: + if idx, ok := pm[t.Name]; ok { + runtimeInstParams = append(runtimeInstParams, instParams[idx]) + continue + } + } + modelType, err := p.parseType(pkg, instParam, tps) + if err != nil { + return nil, err + } + runtimeInstParams = append(runtimeInstParams, modelType) + } + + return runtimeInstParams, nil +} + +func (p *fileParser) constructTps(it *namedInterface) (tps map[string]model.Type) { + tps = make(map[string]model.Type) + n := 0 + for _, tp := range it.typeParams { + for _, tm := range tp.Names { + tps[tm.Name] = nil + if len(it.instTypes) != 0 { + tps[tm.Name] = it.instTypes[n] + n++ + } + } + } + return tps +} + +// parseInterface loads interface specified by pkg and name, parses it and returns +// a new model with the parsed. +func (p *fileParser) parseInterface(name, pkg string, it *namedInterface) (*model.Interface, error) { + iface := &model.Interface{Name: name} + tps := p.constructTps(it) + tp, err := p.parseFieldList(pkg, it.typeParams, tps) + if err != nil { + return nil, fmt.Errorf("unable to parse interface type parameters: %v", name) + } + + iface.TypeParams = tp + for _, field := range it.it.Methods.List { + var methods []*model.Method + if methods, err = p.parseMethod(field, it, iface, pkg, tps); err != nil { + return nil, err + } + for _, m := range methods { + iface.AddMethod(m) + } + } + return iface, nil +} + +func (p *fileParser) parseMethod(field *ast.Field, it *namedInterface, iface *model.Interface, pkg string, tps map[string]model.Type) ([]*model.Method, error) { + // {} for git diff + { + switch v := field.Type.(type) { + case *ast.FuncType: + if nn := len(field.Names); nn != 1 { + return nil, fmt.Errorf("expected one name for interface %v, got %d", iface.Name, nn) + } + m := &model.Method{ + Name: field.Names[0].String(), + } + var err error + m.In, m.Variadic, m.Out, err = p.parseFunc(pkg, v, tps) + if err != nil { + return nil, err + } + return []*model.Method{m}, nil + case *ast.Ident: + // Embedded interface in this package. + embeddedIfaceType := p.auxInterfaces.Get(pkg, v.String()) + if embeddedIfaceType == nil { + embeddedIfaceType = p.importedInterfaces.Get(pkg, v.String()) + } + + var embeddedIface *model.Interface + if embeddedIfaceType != nil { + var err error + embeddedIfaceType.instTypes, err = p.constructInstParams(pkg, it.typeParams, it.instTypes, it.embeddedInstTypeParams, tps) + if err != nil { + return nil, err + } + embeddedIface, err = p.parseInterface(v.String(), pkg, embeddedIfaceType) + if err != nil { + return nil, err + } + + } else { + // This is built-in error interface. + if v.String() == model.ErrorInterface.Name { + embeddedIface = &model.ErrorInterface + } else { + ip, err := p.parsePackage(pkg) + if err != nil { + return nil, p.errorf(v.Pos(), "could not parse package %s: %v", pkg, err) + } + + if embeddedIfaceType = ip.importedInterfaces.Get(pkg, v.String()); embeddedIfaceType == nil { + return nil, p.errorf(v.Pos(), "unknown embedded interface %s.%s", pkg, v.String()) + } + + embeddedIfaceType.instTypes, err = p.constructInstParams(pkg, it.typeParams, it.instTypes, it.embeddedInstTypeParams, tps) + if err != nil { + return nil, err + } + embeddedIface, err = ip.parseInterface(v.String(), pkg, embeddedIfaceType) + if err != nil { + return nil, err + } + } + } + return embeddedIface.Methods, nil + case *ast.SelectorExpr: + // Embedded interface in another package. + filePkg, sel := v.X.(*ast.Ident).String(), v.Sel.String() + embeddedPkg, ok := p.imports[filePkg] + if !ok { + return nil, p.errorf(v.X.Pos(), "unknown package %s", filePkg) + } + + var embeddedIface *model.Interface + var err error + embeddedIfaceType := p.auxInterfaces.Get(filePkg, sel) + if embeddedIfaceType != nil { + embeddedIfaceType.instTypes, err = p.constructInstParams(pkg, it.typeParams, it.instTypes, it.embeddedInstTypeParams, tps) + if err != nil { + return nil, err + } + embeddedIface, err = p.parseInterface(sel, filePkg, embeddedIfaceType) + if err != nil { + return nil, err + } + } else { + path := embeddedPkg.Path() + parser := embeddedPkg.Parser() + if parser == nil { + ip, err := p.parsePackage(path) + if err != nil { + return nil, p.errorf(v.Pos(), "could not parse package %s: %v", path, err) + } + parser = ip + p.imports[filePkg] = importedPkg{ + path: embeddedPkg.Path(), + parser: parser, + } + } + if embeddedIfaceType = parser.importedInterfaces.Get(path, sel); embeddedIfaceType == nil { + return nil, p.errorf(v.Pos(), "unknown embedded interface %s.%s", path, sel) + } + + embeddedIfaceType.instTypes, err = p.constructInstParams(pkg, it.typeParams, it.instTypes, it.embeddedInstTypeParams, tps) + if err != nil { + return nil, err + } + embeddedIface, err = parser.parseInterface(sel, path, embeddedIfaceType) + if err != nil { + return nil, err + } + } + // TODO: apply shadowing rules. + return embeddedIface.Methods, nil + default: + return p.parseGenericMethod(field, it, iface, pkg, tps) + } + } +} + +func (p *fileParser) parseFunc(pkg string, f *ast.FuncType, tps map[string]model.Type) (inParam []*model.Parameter, variadic *model.Parameter, outParam []*model.Parameter, err error) { + if f.Params != nil { + regParams := f.Params.List + if isVariadic(f) { + n := len(regParams) + varParams := regParams[n-1:] + regParams = regParams[:n-1] + vp, err := p.parseFieldList(pkg, varParams, tps) + if err != nil { + return nil, nil, nil, p.errorf(varParams[0].Pos(), "failed parsing variadic argument: %v", err) + } + variadic = vp[0] + } + inParam, err = p.parseFieldList(pkg, regParams, tps) + if err != nil { + return nil, nil, nil, p.errorf(f.Pos(), "failed parsing arguments: %v", err) + } + } + if f.Results != nil { + outParam, err = p.parseFieldList(pkg, f.Results.List, tps) + if err != nil { + return nil, nil, nil, p.errorf(f.Pos(), "failed parsing returns: %v", err) + } + } + return +} + +func (p *fileParser) parseFieldList(pkg string, fields []*ast.Field, tps map[string]model.Type) ([]*model.Parameter, error) { + nf := 0 + for _, f := range fields { + nn := len(f.Names) + if nn == 0 { + nn = 1 // anonymous parameter + } + nf += nn + } + if nf == 0 { + return nil, nil + } + ps := make([]*model.Parameter, nf) + i := 0 // destination index + for _, f := range fields { + t, err := p.parseType(pkg, f.Type, tps) + if err != nil { + return nil, err + } + + if len(f.Names) == 0 { + // anonymous arg + ps[i] = &model.Parameter{Type: t} + i++ + continue + } + for _, name := range f.Names { + ps[i] = &model.Parameter{Name: name.Name, Type: t} + i++ + } + } + return ps, nil +} + +func (p *fileParser) parseType(pkg string, typ ast.Expr, tps map[string]model.Type) (model.Type, error) { + switch v := typ.(type) { + case *ast.ArrayType: + ln := -1 + if v.Len != nil { + value, err := p.parseArrayLength(v.Len) + if err != nil { + return nil, err + } + ln, err = strconv.Atoi(value) + if err != nil { + return nil, p.errorf(v.Len.Pos(), "bad array size: %v", err) + } + } + t, err := p.parseType(pkg, v.Elt, tps) + if err != nil { + return nil, err + } + return &model.ArrayType{Len: ln, Type: t}, nil + case *ast.ChanType: + t, err := p.parseType(pkg, v.Value, tps) + if err != nil { + return nil, err + } + var dir model.ChanDir + if v.Dir == ast.SEND { + dir = model.SendDir + } + if v.Dir == ast.RECV { + dir = model.RecvDir + } + return &model.ChanType{Dir: dir, Type: t}, nil + case *ast.Ellipsis: + // assume we're parsing a variadic argument + return p.parseType(pkg, v.Elt, tps) + case *ast.FuncType: + in, variadic, out, err := p.parseFunc(pkg, v, tps) + if err != nil { + return nil, err + } + return &model.FuncType{In: in, Out: out, Variadic: variadic}, nil + case *ast.Ident: + it, ok := tps[v.Name] + if v.IsExported() && !ok { + // `pkg` may be an aliased imported pkg + // if so, patch the import w/ the fully qualified import + maybeImportedPkg, ok := p.imports[pkg] + if ok { + pkg = maybeImportedPkg.Path() + } + // assume type in this package + return &model.NamedType{Package: pkg, Type: v.Name}, nil + } + if ok && it != nil { + return it, nil + } + // assume predeclared type + return model.PredeclaredType(v.Name), nil + case *ast.InterfaceType: + if v.Methods != nil && len(v.Methods.List) > 0 { + return nil, p.errorf(v.Pos(), "can't handle non-empty unnamed interface types") + } + return model.PredeclaredType("any"), nil + case *ast.MapType: + key, err := p.parseType(pkg, v.Key, tps) + if err != nil { + return nil, err + } + value, err := p.parseType(pkg, v.Value, tps) + if err != nil { + return nil, err + } + return &model.MapType{Key: key, Value: value}, nil + case *ast.SelectorExpr: + pkgName := v.X.(*ast.Ident).String() + pkg, ok := p.imports[pkgName] + if !ok { + return nil, p.errorf(v.Pos(), "unknown package %q", pkgName) + } + return &model.NamedType{Package: pkg.Path(), Type: v.Sel.String()}, nil + case *ast.StarExpr: + t, err := p.parseType(pkg, v.X, tps) + if err != nil { + return nil, err + } + return &model.PointerType{Type: t}, nil + case *ast.StructType: + if v.Fields != nil && len(v.Fields.List) > 0 { + return nil, p.errorf(v.Pos(), "can't handle non-empty unnamed struct types") + } + return model.PredeclaredType("struct{}"), nil + case *ast.ParenExpr: + return p.parseType(pkg, v.X, tps) + default: + mt, err := p.parseGenericType(pkg, typ, tps) + if err != nil { + return nil, err + } + if mt == nil { + break + } + return mt, nil + } + + return nil, fmt.Errorf("don't know how to parse type %T", typ) +} + +func (p *fileParser) parseArrayLength(expr ast.Expr) (string, error) { + switch val := expr.(type) { + case (*ast.BasicLit): + return val.Value, nil + case (*ast.Ident): + // when the length is a const defined locally + return val.Obj.Decl.(*ast.ValueSpec).Values[0].(*ast.BasicLit).Value, nil + case (*ast.SelectorExpr): + // when the length is a const defined in an external package + usedPkg, err := importer.Default().Import(fmt.Sprintf("%s", val.X)) + if err != nil { + return "", p.errorf(expr.Pos(), "unknown package in array length: %v", err) + } + ev, err := types.Eval(token.NewFileSet(), usedPkg, token.NoPos, val.Sel.Name) + if err != nil { + return "", p.errorf(expr.Pos(), "unknown constant in array length: %v", err) + } + return ev.Value.String(), nil + case (*ast.ParenExpr): + return p.parseArrayLength(val.X) + case (*ast.BinaryExpr): + x, err := p.parseArrayLength(val.X) + if err != nil { + return "", err + } + y, err := p.parseArrayLength(val.Y) + if err != nil { + return "", err + } + biExpr := fmt.Sprintf("%s%v%s", x, val.Op, y) + tv, err := types.Eval(token.NewFileSet(), nil, token.NoPos, biExpr) + if err != nil { + return "", p.errorf(expr.Pos(), "invalid expression in array length: %v", err) + } + return tv.Value.String(), nil + default: + return "", p.errorf(expr.Pos(), "invalid expression in array length: %v", val) + } +} + +// importsOfFile returns a map of package name to import path +// of the imports in file. +func importsOfFile(file *ast.File) (normalImports map[string]importedPackage, dotImports []string) { + var importPaths []string + for _, is := range file.Imports { + if is.Name != nil { + continue + } + importPath := is.Path.Value[1 : len(is.Path.Value)-1] // remove quotes + importPaths = append(importPaths, importPath) + } + packagesName := createPackageMap(importPaths) + normalImports = make(map[string]importedPackage) + dotImports = make([]string, 0) + for _, is := range file.Imports { + var pkgName string + importPath := is.Path.Value[1 : len(is.Path.Value)-1] // remove quotes + + if is.Name != nil { + // Named imports are always certain. + if is.Name.Name == "_" { + continue + } + pkgName = is.Name.Name + } else { + pkg, ok := packagesName[importPath] + if !ok { + // Fallback to import path suffix. Note that this is uncertain. + _, last := path.Split(importPath) + // If the last path component has dots, the first dot-delimited + // field is used as the name. + pkgName = strings.SplitN(last, ".", 2)[0] + } else { + pkgName = pkg + } + } + + if pkgName == "." { + dotImports = append(dotImports, importPath) + } else { + if pkg, ok := normalImports[pkgName]; ok { + switch p := pkg.(type) { + case duplicateImport: + normalImports[pkgName] = duplicateImport{ + name: p.name, + duplicates: append([]string{importPath}, p.duplicates...), + } + case importedPkg: + normalImports[pkgName] = duplicateImport{ + name: pkgName, + duplicates: []string{p.path, importPath}, + } + } + } else { + normalImports[pkgName] = importedPkg{path: importPath} + } + } + } + return +} + +type namedInterface struct { + name *ast.Ident + it *ast.InterfaceType + typeParams []*ast.Field + embeddedInstTypeParams []ast.Expr + instTypes []model.Type +} + +// Create an iterator over all interfaces in file. +func iterInterfaces(file *ast.File) <-chan *namedInterface { + ch := make(chan *namedInterface) + go func() { + for _, decl := range file.Decls { + gd, ok := decl.(*ast.GenDecl) + if !ok || gd.Tok != token.TYPE { + continue + } + for _, spec := range gd.Specs { + ts, ok := spec.(*ast.TypeSpec) + if !ok { + continue + } + it, ok := ts.Type.(*ast.InterfaceType) + if !ok { + continue + } + + ch <- &namedInterface{name: ts.Name, it: it, typeParams: getTypeSpecTypeParams(ts)} + } + } + close(ch) + }() + return ch +} + +// isVariadic returns whether the function is variadic. +func isVariadic(f *ast.FuncType) bool { + nargs := len(f.Params.List) + if nargs == 0 { + return false + } + _, ok := f.Params.List[nargs-1].Type.(*ast.Ellipsis) + return ok +} + +// packageNameOfDir get package import path via dir +func packageNameOfDir(srcDir string) (string, error) { + files, err := os.ReadDir(srcDir) + if err != nil { + log.Fatal(err) + } + + var goFilePath string + for _, file := range files { + if !file.IsDir() && strings.HasSuffix(file.Name(), ".go") { + goFilePath = file.Name() + break + } + } + if goFilePath == "" { + return "", fmt.Errorf("go source file not found %s", srcDir) + } + + packageImport, err := parsePackageImport(srcDir) + if err != nil { + return "", err + } + return packageImport, nil +} + +var errOutsideGoPath = errors.New("source directory is outside GOPATH") diff --git a/vendor/go.uber.org/mock/mockgen/reflect.go b/vendor/go.uber.org/mock/mockgen/reflect.go new file mode 100644 index 00000000..ca80ebbb --- /dev/null +++ b/vendor/go.uber.org/mock/mockgen/reflect.go @@ -0,0 +1,256 @@ +// Copyright 2012 Google Inc. +// +// 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. + +package main + +// This file contains the model construction by reflection. + +import ( + "bytes" + "encoding/gob" + "flag" + "fmt" + "go/build" + "io" + "log" + "os" + "os/exec" + "path/filepath" + "runtime" + "strings" + "text/template" + + "go.uber.org/mock/mockgen/model" +) + +var ( + progOnly = flag.Bool("prog_only", false, "(reflect mode) Only generate the reflection program; write it to stdout and exit.") + execOnly = flag.String("exec_only", "", "(reflect mode) If set, execute this reflection program.") + buildFlags = flag.String("build_flags", "", "(reflect mode) Additional flags for go build.") +) + +// reflectMode generates mocks via reflection on an interface. +func reflectMode(importPath string, symbols []string) (*model.Package, error) { + if *execOnly != "" { + return run(*execOnly) + } + + program, err := writeProgram(importPath, symbols) + if err != nil { + return nil, err + } + + if *progOnly { + if _, err := os.Stdout.Write(program); err != nil { + return nil, err + } + os.Exit(0) + } + + wd, _ := os.Getwd() + + // Try to run the reflection program in the current working directory. + if p, err := runInDir(program, wd); err == nil { + return p, nil + } + + // Try to run the program in the same directory as the input package. + if p, err := build.Import(importPath, wd, build.FindOnly); err == nil { + dir := p.Dir + if p, err := runInDir(program, dir); err == nil { + return p, nil + } + } + + // Try to run it in a standard temp directory. + return runInDir(program, "") +} + +func writeProgram(importPath string, symbols []string) ([]byte, error) { + var program bytes.Buffer + data := reflectData{ + ImportPath: importPath, + Symbols: symbols, + } + if err := reflectProgram.Execute(&program, &data); err != nil { + return nil, err + } + return program.Bytes(), nil +} + +// run the given program and parse the output as a model.Package. +func run(program string) (*model.Package, error) { + f, err := os.CreateTemp("", "") + if err != nil { + return nil, err + } + + filename := f.Name() + defer os.Remove(filename) + if err := f.Close(); err != nil { + return nil, err + } + + // Run the program. + cmd := exec.Command(program, "-output", filename) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + return nil, err + } + + f, err = os.Open(filename) + if err != nil { + return nil, err + } + + // Process output. + var pkg model.Package + if err := gob.NewDecoder(f).Decode(&pkg); err != nil { + return nil, err + } + + if err := f.Close(); err != nil { + return nil, err + } + + return &pkg, nil +} + +// runInDir writes the given program into the given dir, runs it there, and +// parses the output as a model.Package. +func runInDir(program []byte, dir string) (*model.Package, error) { + // We use TempDir instead of TempFile so we can control the filename. + tmpDir, err := os.MkdirTemp(dir, "gomock_reflect_") + if err != nil { + return nil, err + } + defer func() { + if err := os.RemoveAll(tmpDir); err != nil { + log.Printf("failed to remove temp directory: %s", err) + } + }() + const progSource = "prog.go" + var progBinary = "prog.bin" + if runtime.GOOS == "windows" { + // Windows won't execute a program unless it has a ".exe" suffix. + progBinary += ".exe" + } + + if err := os.WriteFile(filepath.Join(tmpDir, progSource), program, 0600); err != nil { + return nil, err + } + + cmdArgs := []string{} + cmdArgs = append(cmdArgs, "build") + if *buildFlags != "" { + cmdArgs = append(cmdArgs, strings.Split(*buildFlags, " ")...) + } + cmdArgs = append(cmdArgs, "-o", progBinary, progSource) + + // Build the program. + buf := bytes.NewBuffer(nil) + cmd := exec.Command("go", cmdArgs...) + cmd.Dir = tmpDir + cmd.Stdout = os.Stdout + cmd.Stderr = io.MultiWriter(os.Stderr, buf) + if err := cmd.Run(); err != nil { + sErr := buf.String() + if strings.Contains(sErr, `cannot find package "."`) && + strings.Contains(sErr, "go.uber.org/mock/mockgen/model") { + fmt.Fprint(os.Stderr, "Please reference the steps in the README to fix this error:\n\thttps://go.uber.org/mock#reflect-vendoring-error.\n") + return nil, err + } + return nil, err + } + + return run(filepath.Join(tmpDir, progBinary)) +} + +type reflectData struct { + ImportPath string + Symbols []string +} + +// This program reflects on an interface value, and prints the +// gob encoding of a model.Package to standard output. +// JSON doesn't work because of the model.Type interface. +var reflectProgram = template.Must(template.New("program").Parse(` +// Code generated by MockGen. DO NOT EDIT. +package main + +import ( + "encoding/gob" + "flag" + "fmt" + "os" + "path" + "reflect" + + "go.uber.org/mock/mockgen/model" + + pkg_ {{printf "%q" .ImportPath}} +) + +var output = flag.String("output", "", "The output file name, or empty to use stdout.") + +func main() { + flag.Parse() + + its := []struct{ + sym string + typ reflect.Type + }{ + {{range .Symbols}} + { {{printf "%q" .}}, reflect.TypeOf((*pkg_.{{.}})(nil)).Elem()}, + {{end}} + } + pkg := &model.Package{ + // NOTE: This behaves contrary to documented behaviour if the + // package name is not the final component of the import path. + // The reflect package doesn't expose the package name, though. + Name: path.Base({{printf "%q" .ImportPath}}), + } + + for _, it := range its { + intf, err := model.InterfaceFromInterfaceType(it.typ) + if err != nil { + fmt.Fprintf(os.Stderr, "Reflection: %v\n", err) + os.Exit(1) + } + intf.Name = it.sym + pkg.Interfaces = append(pkg.Interfaces, intf) + } + + outfile := os.Stdout + if len(*output) != 0 { + var err error + outfile, err = os.Create(*output) + if err != nil { + fmt.Fprintf(os.Stderr, "failed to open output file %q", *output) + } + defer func() { + if err := outfile.Close(); err != nil { + fmt.Fprintf(os.Stderr, "failed to close output file %q", *output) + os.Exit(1) + } + }() + } + + if err := gob.NewEncoder(outfile).Encode(pkg); err != nil { + fmt.Fprintf(os.Stderr, "gob encode: %v\n", err) + os.Exit(1) + } +} +`)) diff --git a/vendor/go.uber.org/mock/mockgen/version.go b/vendor/go.uber.org/mock/mockgen/version.go new file mode 100644 index 00000000..6db160ac --- /dev/null +++ b/vendor/go.uber.org/mock/mockgen/version.go @@ -0,0 +1,31 @@ +// Copyright 2022 Google LLC +// +// 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. + +package main + +import ( + "fmt" + "log" + "runtime/debug" +) + +func printModuleVersion() { + if bi, exists := debug.ReadBuildInfo(); exists { + fmt.Println(bi.Main.Version) + } else { + log.Printf("No version information found. Make sure to use " + + "GO111MODULE=on when running 'go get' in order to use specific " + + "version of the binary.") + } +} 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 00000000..2a0123d4 --- /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 00000000..22056825 --- /dev/null +++ b/vendor/golang.org/x/mod/modfile/read.go @@ -0,0 +1,958 @@ +// 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. + line := &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] = line + 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() + 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 00000000..66dcaf98 --- /dev/null +++ b/vendor/golang.org/x/mod/modfile/rule.go @@ -0,0 +1,1766 @@ +// 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 + + 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 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": + 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) + } +} + +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 +} + +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 and replace directives. +// +// Earlier exclude 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) +} + +func removeDups(syntax *FileSyntax, exclude *[]*Exclude, replace *[]*Replace) { + 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 + + // 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 00000000..8f54897c --- /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) +} diff --git a/vendor/golang.org/x/mod/module/module.go b/vendor/golang.org/x/mod/module/module.go index 2a364b22..cac1a899 100644 --- a/vendor/golang.org/x/mod/module/module.go +++ b/vendor/golang.org/x/mod/module/module.go @@ -506,6 +506,7 @@ var badWindowsNames = []string{ "PRN", "AUX", "NUL", + "COM0", "COM1", "COM2", "COM3", @@ -515,6 +516,7 @@ var badWindowsNames = []string{ "COM7", "COM8", "COM9", + "LPT0", "LPT1", "LPT2", "LPT3", diff --git a/vendor/golang.org/x/sys/unix/mkerrors.sh b/vendor/golang.org/x/sys/unix/mkerrors.sh index fdcaa974..4ed2e488 100644 --- a/vendor/golang.org/x/sys/unix/mkerrors.sh +++ b/vendor/golang.org/x/sys/unix/mkerrors.sh @@ -263,6 +263,7 @@ struct ltchars { #include #include #include +#include #include #include #include @@ -549,6 +550,7 @@ ccflags="$@" $2 !~ "NLA_TYPE_MASK" && $2 !~ /^RTC_VL_(ACCURACY|BACKUP|DATA)/ && $2 ~ /^(NETLINK|NLM|NLMSG|NLA|IFA|IFAN|RT|RTC|RTCF|RTN|RTPROT|RTNH|ARPHRD|ETH_P|NETNSA)_/ || + $2 ~ /^SOCK_|SK_DIAG_|SKNLGRP_$/ || $2 ~ /^FIORDCHK$/ || $2 ~ /^SIOC/ || $2 ~ /^TIOC/ || diff --git a/vendor/golang.org/x/sys/unix/zerrors_linux.go b/vendor/golang.org/x/sys/unix/zerrors_linux.go index 93a38a97..877a62b4 100644 --- a/vendor/golang.org/x/sys/unix/zerrors_linux.go +++ b/vendor/golang.org/x/sys/unix/zerrors_linux.go @@ -502,6 +502,7 @@ const ( BPF_IMM = 0x0 BPF_IND = 0x40 BPF_JA = 0x0 + BPF_JCOND = 0xe0 BPF_JEQ = 0x10 BPF_JGE = 0x30 BPF_JGT = 0x20 @@ -657,6 +658,9 @@ const ( CAN_NPROTO = 0x8 CAN_RAW = 0x1 CAN_RAW_FILTER_MAX = 0x200 + CAN_RAW_XL_VCID_RX_FILTER = 0x4 + CAN_RAW_XL_VCID_TX_PASS = 0x2 + CAN_RAW_XL_VCID_TX_SET = 0x1 CAN_RTR_FLAG = 0x40000000 CAN_SFF_ID_BITS = 0xb CAN_SFF_MASK = 0x7ff @@ -1339,6 +1343,7 @@ const ( F_OFD_SETLK = 0x25 F_OFD_SETLKW = 0x26 F_OK = 0x0 + F_SEAL_EXEC = 0x20 F_SEAL_FUTURE_WRITE = 0x10 F_SEAL_GROW = 0x4 F_SEAL_SEAL = 0x1 @@ -1627,6 +1632,7 @@ const ( IP_FREEBIND = 0xf IP_HDRINCL = 0x3 IP_IPSEC_POLICY = 0x10 + IP_LOCAL_PORT_RANGE = 0x33 IP_MAXPACKET = 0xffff IP_MAX_MEMBERSHIPS = 0x14 IP_MF = 0x2000 @@ -1653,6 +1659,7 @@ const ( IP_PMTUDISC_OMIT = 0x5 IP_PMTUDISC_PROBE = 0x3 IP_PMTUDISC_WANT = 0x1 + IP_PROTOCOL = 0x34 IP_RECVERR = 0xb IP_RECVERR_RFC4884 = 0x1a IP_RECVFRAGSIZE = 0x19 @@ -2169,7 +2176,7 @@ const ( NFT_SECMARK_CTX_MAXLEN = 0x100 NFT_SET_MAXNAMELEN = 0x100 NFT_SOCKET_MAX = 0x3 - NFT_TABLE_F_MASK = 0x3 + NFT_TABLE_F_MASK = 0x7 NFT_TABLE_MAXNAMELEN = 0x100 NFT_TRACETYPE_MAX = 0x3 NFT_TUNNEL_F_MASK = 0x7 @@ -2403,6 +2410,7 @@ const ( PERF_RECORD_MISC_USER = 0x2 PERF_SAMPLE_BRANCH_PLM_ALL = 0x7 PERF_SAMPLE_WEIGHT_TYPE = 0x1004000 + PID_FS_MAGIC = 0x50494446 PIPEFS_MAGIC = 0x50495045 PPPIOCGNPMODE = 0xc008744c PPPIOCNEWUNIT = 0xc004743e @@ -2896,8 +2904,9 @@ const ( RWF_APPEND = 0x10 RWF_DSYNC = 0x2 RWF_HIPRI = 0x1 + RWF_NOAPPEND = 0x20 RWF_NOWAIT = 0x8 - RWF_SUPPORTED = 0x1f + RWF_SUPPORTED = 0x3f RWF_SYNC = 0x4 RWF_WRITE_LIFE_NOT_SET = 0x0 SCHED_BATCH = 0x3 @@ -2918,7 +2927,9 @@ const ( SCHED_RESET_ON_FORK = 0x40000000 SCHED_RR = 0x2 SCM_CREDENTIALS = 0x2 + SCM_PIDFD = 0x4 SCM_RIGHTS = 0x1 + SCM_SECURITY = 0x3 SCM_TIMESTAMP = 0x1d SC_LOG_FLUSH = 0x100000 SECCOMP_ADDFD_FLAG_SEND = 0x2 @@ -3051,6 +3062,8 @@ const ( SIOCSMIIREG = 0x8949 SIOCSRARP = 0x8962 SIOCWANDEV = 0x894a + SK_DIAG_BPF_STORAGE_MAX = 0x3 + SK_DIAG_BPF_STORAGE_REQ_MAX = 0x1 SMACK_MAGIC = 0x43415d53 SMART_AUTOSAVE = 0xd2 SMART_AUTO_OFFLINE = 0xdb @@ -3071,6 +3084,8 @@ const ( SOCKFS_MAGIC = 0x534f434b SOCK_BUF_LOCK_MASK = 0x3 SOCK_DCCP = 0x6 + SOCK_DESTROY = 0x15 + SOCK_DIAG_BY_FAMILY = 0x14 SOCK_IOC_TYPE = 0x89 SOCK_PACKET = 0xa SOCK_RAW = 0x3 @@ -3260,6 +3275,7 @@ const ( TCP_MAX_WINSHIFT = 0xe TCP_MD5SIG = 0xe TCP_MD5SIG_EXT = 0x20 + TCP_MD5SIG_FLAG_IFINDEX = 0x2 TCP_MD5SIG_FLAG_PREFIX = 0x1 TCP_MD5SIG_MAXKEYLEN = 0x50 TCP_MSS = 0x200 diff --git a/vendor/golang.org/x/sys/unix/zerrors_linux_386.go b/vendor/golang.org/x/sys/unix/zerrors_linux_386.go index 42ff8c3c..e4bc0bd5 100644 --- a/vendor/golang.org/x/sys/unix/zerrors_linux_386.go +++ b/vendor/golang.org/x/sys/unix/zerrors_linux_386.go @@ -118,6 +118,7 @@ const ( IXOFF = 0x1000 IXON = 0x400 MAP_32BIT = 0x40 + MAP_ABOVE4G = 0x80 MAP_ANON = 0x20 MAP_ANONYMOUS = 0x20 MAP_DENYWRITE = 0x800 diff --git a/vendor/golang.org/x/sys/unix/zerrors_linux_amd64.go b/vendor/golang.org/x/sys/unix/zerrors_linux_amd64.go index dca43600..689317af 100644 --- a/vendor/golang.org/x/sys/unix/zerrors_linux_amd64.go +++ b/vendor/golang.org/x/sys/unix/zerrors_linux_amd64.go @@ -118,6 +118,7 @@ const ( IXOFF = 0x1000 IXON = 0x400 MAP_32BIT = 0x40 + MAP_ABOVE4G = 0x80 MAP_ANON = 0x20 MAP_ANONYMOUS = 0x20 MAP_DENYWRITE = 0x800 diff --git a/vendor/golang.org/x/sys/unix/zerrors_linux_arm64.go b/vendor/golang.org/x/sys/unix/zerrors_linux_arm64.go index d8cae6d1..14270508 100644 --- a/vendor/golang.org/x/sys/unix/zerrors_linux_arm64.go +++ b/vendor/golang.org/x/sys/unix/zerrors_linux_arm64.go @@ -87,6 +87,7 @@ const ( FICLONE = 0x40049409 FICLONERANGE = 0x4020940d FLUSHO = 0x1000 + FPMR_MAGIC = 0x46504d52 FPSIMD_MAGIC = 0x46508001 FS_IOC_ENABLE_VERITY = 0x40806685 FS_IOC_GETFLAGS = 0x80086601 diff --git a/vendor/golang.org/x/sys/unix/ztypes_linux.go b/vendor/golang.org/x/sys/unix/ztypes_linux.go index 0036746e..4740b834 100644 --- a/vendor/golang.org/x/sys/unix/ztypes_linux.go +++ b/vendor/golang.org/x/sys/unix/ztypes_linux.go @@ -4605,7 +4605,7 @@ const ( NL80211_ATTR_MAC_HINT = 0xc8 NL80211_ATTR_MAC_MASK = 0xd7 NL80211_ATTR_MAX_AP_ASSOC_STA = 0xca - NL80211_ATTR_MAX = 0x149 + NL80211_ATTR_MAX = 0x14a NL80211_ATTR_MAX_CRIT_PROT_DURATION = 0xb4 NL80211_ATTR_MAX_CSA_COUNTERS = 0xce NL80211_ATTR_MAX_MATCH_SETS = 0x85 @@ -5209,7 +5209,7 @@ const ( NL80211_FREQUENCY_ATTR_GO_CONCURRENT = 0xf NL80211_FREQUENCY_ATTR_INDOOR_ONLY = 0xe NL80211_FREQUENCY_ATTR_IR_CONCURRENT = 0xf - NL80211_FREQUENCY_ATTR_MAX = 0x1f + NL80211_FREQUENCY_ATTR_MAX = 0x20 NL80211_FREQUENCY_ATTR_MAX_TX_POWER = 0x6 NL80211_FREQUENCY_ATTR_NO_10MHZ = 0x11 NL80211_FREQUENCY_ATTR_NO_160MHZ = 0xc @@ -5703,7 +5703,7 @@ const ( NL80211_STA_FLAG_ASSOCIATED = 0x7 NL80211_STA_FLAG_AUTHENTICATED = 0x5 NL80211_STA_FLAG_AUTHORIZED = 0x1 - NL80211_STA_FLAG_MAX = 0x7 + NL80211_STA_FLAG_MAX = 0x8 NL80211_STA_FLAG_MAX_OLD_API = 0x6 NL80211_STA_FLAG_MFP = 0x4 NL80211_STA_FLAG_SHORT_PREAMBLE = 0x2 @@ -6001,3 +6001,34 @@ type CachestatRange struct { Off uint64 Len uint64 } + +const ( + SK_MEMINFO_RMEM_ALLOC = 0x0 + SK_MEMINFO_RCVBUF = 0x1 + SK_MEMINFO_WMEM_ALLOC = 0x2 + SK_MEMINFO_SNDBUF = 0x3 + SK_MEMINFO_FWD_ALLOC = 0x4 + SK_MEMINFO_WMEM_QUEUED = 0x5 + SK_MEMINFO_OPTMEM = 0x6 + SK_MEMINFO_BACKLOG = 0x7 + SK_MEMINFO_DROPS = 0x8 + SK_MEMINFO_VARS = 0x9 + SKNLGRP_NONE = 0x0 + SKNLGRP_INET_TCP_DESTROY = 0x1 + SKNLGRP_INET_UDP_DESTROY = 0x2 + SKNLGRP_INET6_TCP_DESTROY = 0x3 + SKNLGRP_INET6_UDP_DESTROY = 0x4 + SK_DIAG_BPF_STORAGE_REQ_NONE = 0x0 + SK_DIAG_BPF_STORAGE_REQ_MAP_FD = 0x1 + SK_DIAG_BPF_STORAGE_REP_NONE = 0x0 + SK_DIAG_BPF_STORAGE = 0x1 + SK_DIAG_BPF_STORAGE_NONE = 0x0 + SK_DIAG_BPF_STORAGE_PAD = 0x1 + SK_DIAG_BPF_STORAGE_MAP_ID = 0x2 + SK_DIAG_BPF_STORAGE_MAP_VALUE = 0x3 +) + +type SockDiagReq struct { + Family uint8 + Protocol uint8 +} diff --git a/vendor/golang.org/x/sys/windows/security_windows.go b/vendor/golang.org/x/sys/windows/security_windows.go index 26be94a8..6f7d2ac7 100644 --- a/vendor/golang.org/x/sys/windows/security_windows.go +++ b/vendor/golang.org/x/sys/windows/security_windows.go @@ -68,6 +68,7 @@ type UserInfo10 struct { //sys NetUserGetInfo(serverName *uint16, userName *uint16, level uint32, buf **byte) (neterr error) = netapi32.NetUserGetInfo //sys NetGetJoinInformation(server *uint16, name **uint16, bufType *uint32) (neterr error) = netapi32.NetGetJoinInformation //sys NetApiBufferFree(buf *byte) (neterr error) = netapi32.NetApiBufferFree +//sys NetUserEnum(serverName *uint16, level uint32, filter uint32, buf **byte, prefMaxLen uint32, entriesRead *uint32, totalEntries *uint32, resumeHandle *uint32) (neterr error) = netapi32.NetUserEnum const ( // do not reorder diff --git a/vendor/golang.org/x/sys/windows/zsyscall_windows.go b/vendor/golang.org/x/sys/windows/zsyscall_windows.go index 5c6035dd..9f73df75 100644 --- a/vendor/golang.org/x/sys/windows/zsyscall_windows.go +++ b/vendor/golang.org/x/sys/windows/zsyscall_windows.go @@ -401,6 +401,7 @@ var ( procTransmitFile = modmswsock.NewProc("TransmitFile") procNetApiBufferFree = modnetapi32.NewProc("NetApiBufferFree") procNetGetJoinInformation = modnetapi32.NewProc("NetGetJoinInformation") + procNetUserEnum = modnetapi32.NewProc("NetUserEnum") procNetUserGetInfo = modnetapi32.NewProc("NetUserGetInfo") procNtCreateFile = modntdll.NewProc("NtCreateFile") procNtCreateNamedPipeFile = modntdll.NewProc("NtCreateNamedPipeFile") @@ -3486,6 +3487,14 @@ func NetGetJoinInformation(server *uint16, name **uint16, bufType *uint32) (nete return } +func NetUserEnum(serverName *uint16, level uint32, filter uint32, buf **byte, prefMaxLen uint32, entriesRead *uint32, totalEntries *uint32, resumeHandle *uint32) (neterr error) { + r0, _, _ := syscall.Syscall9(procNetUserEnum.Addr(), 8, uintptr(unsafe.Pointer(serverName)), uintptr(level), uintptr(filter), uintptr(unsafe.Pointer(buf)), uintptr(prefMaxLen), uintptr(unsafe.Pointer(entriesRead)), uintptr(unsafe.Pointer(totalEntries)), uintptr(unsafe.Pointer(resumeHandle)), 0) + if r0 != 0 { + neterr = syscall.Errno(r0) + } + return +} + func NetUserGetInfo(serverName *uint16, userName *uint16, level uint32, buf **byte) (neterr error) { r0, _, _ := syscall.Syscall6(procNetUserGetInfo.Addr(), 4, uintptr(unsafe.Pointer(serverName)), uintptr(unsafe.Pointer(userName)), uintptr(level), uintptr(unsafe.Pointer(buf)), 0, 0) if r0 != 0 { diff --git a/vendor/golang.org/x/tools/internal/gocommand/invoke.go b/vendor/golang.org/x/tools/internal/gocommand/invoke.go index eb7a8282..af0ee6c6 100644 --- a/vendor/golang.org/x/tools/internal/gocommand/invoke.go +++ b/vendor/golang.org/x/tools/internal/gocommand/invoke.go @@ -8,12 +8,14 @@ package gocommand import ( "bytes" "context" + "encoding/json" "errors" "fmt" "io" "log" "os" "os/exec" + "path/filepath" "reflect" "regexp" "runtime" @@ -167,7 +169,9 @@ type Invocation struct { // TODO(rfindley): remove, in favor of Args. ModFile string - // If Overlay is set, the go command is invoked with -overlay=Overlay. + // Overlay is the name of the JSON overlay file that describes + // unsaved editor buffers; see [WriteOverlays]. + // If set, the go command is invoked with -overlay=Overlay. // TODO(rfindley): remove, in favor of Args. Overlay string @@ -255,12 +259,15 @@ func (i *Invocation) run(ctx context.Context, stdout, stderr io.Writer) error { waitDelay.Set(reflect.ValueOf(30 * time.Second)) } - // On darwin the cwd gets resolved to the real path, which breaks anything that - // expects the working directory to keep the original path, including the + // The cwd gets resolved to the real path. On Darwin, where + // /tmp is a symlink, this breaks anything that expects the + // working directory to keep the original path, including the // go command when dealing with modules. - // The Go stdlib has a special feature where if the cwd and the PWD are the - // same node then it trusts the PWD, so by setting it in the env for the child - // process we fix up all the paths returned by the go command. + // + // os.Getwd has a special feature where if the cwd and the PWD + // are the same node then it trusts the PWD, so by setting it + // in the env for the child process we fix up all the paths + // returned by the go command. if !i.CleanEnv { cmd.Env = os.Environ() } @@ -351,6 +358,7 @@ func runCmdContext(ctx context.Context, cmd *exec.Cmd) (err error) { } } + startTime := time.Now() err = cmd.Start() if stdoutW != nil { // The child process has inherited the pipe file, @@ -377,7 +385,7 @@ func runCmdContext(ctx context.Context, cmd *exec.Cmd) (err error) { case err := <-resChan: return err case <-timer.C: - HandleHangingGoCommand(cmd.Process) + HandleHangingGoCommand(startTime, cmd) case <-ctx.Done(): } } else { @@ -411,7 +419,7 @@ func runCmdContext(ctx context.Context, cmd *exec.Cmd) (err error) { return <-resChan } -func HandleHangingGoCommand(proc *os.Process) { +func HandleHangingGoCommand(start time.Time, cmd *exec.Cmd) { switch runtime.GOOS { case "linux", "darwin", "freebsd", "netbsd": fmt.Fprintln(os.Stderr, `DETECTED A HANGING GO COMMAND @@ -444,7 +452,7 @@ See golang/go#54461 for more details.`) panic(fmt.Sprintf("running %s: %v", listFiles, err)) } } - panic(fmt.Sprintf("detected hanging go command (pid %d): see golang/go#54461 for more details", proc.Pid)) + panic(fmt.Sprintf("detected hanging go command (golang/go#54461); waited %s\n\tcommand:%s\n\tpid:%d", time.Since(start), cmd, cmd.Process.Pid)) } func cmdDebugStr(cmd *exec.Cmd) string { @@ -468,3 +476,73 @@ func cmdDebugStr(cmd *exec.Cmd) string { } return fmt.Sprintf("GOROOT=%v GOPATH=%v GO111MODULE=%v GOPROXY=%v PWD=%v %v", env["GOROOT"], env["GOPATH"], env["GO111MODULE"], env["GOPROXY"], env["PWD"], strings.Join(args, " ")) } + +// WriteOverlays writes each value in the overlay (see the Overlay +// field of go/packages.Config) to a temporary file and returns the name +// of a JSON file describing the mapping that is suitable for the "go +// list -overlay" flag. +// +// On success, the caller must call the cleanup function exactly once +// when the files are no longer needed. +func WriteOverlays(overlay map[string][]byte) (filename string, cleanup func(), err error) { + // Do nothing if there are no overlays in the config. + if len(overlay) == 0 { + return "", func() {}, nil + } + + dir, err := os.MkdirTemp("", "gocommand-*") + if err != nil { + return "", nil, err + } + + // The caller must clean up this directory, + // unless this function returns an error. + // (The cleanup operand of each return + // statement below is ignored.) + defer func() { + cleanup = func() { + os.RemoveAll(dir) + } + if err != nil { + cleanup() + cleanup = nil + } + }() + + // Write each map entry to a temporary file. + overlays := make(map[string]string) + for k, v := range overlay { + // Use a unique basename for each file (001-foo.go), + // to avoid creating nested directories. + base := fmt.Sprintf("%d-%s.go", 1+len(overlays), filepath.Base(k)) + filename := filepath.Join(dir, base) + err := os.WriteFile(filename, v, 0666) + if err != nil { + return "", nil, err + } + overlays[k] = filename + } + + // Write the JSON overlay file that maps logical file names to temp files. + // + // OverlayJSON is the format overlay files are expected to be in. + // The Replace map maps from overlaid paths to replacement paths: + // the Go command will forward all reads trying to open + // each overlaid path to its replacement path, or consider the overlaid + // path not to exist if the replacement path is empty. + // + // From golang/go#39958. + type OverlayJSON struct { + Replace map[string]string `json:"replace,omitempty"` + } + b, err := json.Marshal(OverlayJSON{Replace: overlays}) + if err != nil { + return "", nil, err + } + filename = filepath.Join(dir, "overlay.json") + if err := os.WriteFile(filename, b, 0666); err != nil { + return "", nil, err + } + + return filename, nil, nil +} diff --git a/vendor/golang.org/x/tools/internal/imports/fix.go b/vendor/golang.org/x/tools/internal/imports/fix.go index 93d49a6e..4569313a 100644 --- a/vendor/golang.org/x/tools/internal/imports/fix.go +++ b/vendor/golang.org/x/tools/internal/imports/fix.go @@ -104,7 +104,10 @@ type packageInfo struct { // parseOtherFiles parses all the Go files in srcDir except filename, including // test files if filename looks like a test. -func parseOtherFiles(fset *token.FileSet, srcDir, filename string) []*ast.File { +// +// It returns an error only if ctx is cancelled. Files with parse errors are +// ignored. +func parseOtherFiles(ctx context.Context, fset *token.FileSet, srcDir, filename string) ([]*ast.File, error) { // This could use go/packages but it doesn't buy much, and it fails // with https://golang.org/issue/26296 in LoadFiles mode in some cases. considerTests := strings.HasSuffix(filename, "_test.go") @@ -112,11 +115,14 @@ func parseOtherFiles(fset *token.FileSet, srcDir, filename string) []*ast.File { fileBase := filepath.Base(filename) packageFileInfos, err := os.ReadDir(srcDir) if err != nil { - return nil + return nil, ctx.Err() } var files []*ast.File for _, fi := range packageFileInfos { + if ctx.Err() != nil { + return nil, ctx.Err() + } if fi.Name() == fileBase || !strings.HasSuffix(fi.Name(), ".go") { continue } @@ -132,7 +138,7 @@ func parseOtherFiles(fset *token.FileSet, srcDir, filename string) []*ast.File { files = append(files, f) } - return files + return files, ctx.Err() } // addGlobals puts the names of package vars into the provided map. @@ -557,12 +563,7 @@ func (p *pass) addCandidate(imp *ImportInfo, pkg *packageInfo) { // fixImports adds and removes imports from f so that all its references are // satisfied and there are no unused imports. -// -// This is declared as a variable rather than a function so goimports can -// easily be extended by adding a file with an init function. -var fixImports = fixImportsDefault - -func fixImportsDefault(fset *token.FileSet, f *ast.File, filename string, env *ProcessEnv) error { +func fixImports(fset *token.FileSet, f *ast.File, filename string, env *ProcessEnv) error { fixes, err := getFixes(context.Background(), fset, f, filename, env) if err != nil { return err @@ -592,7 +593,10 @@ func getFixes(ctx context.Context, fset *token.FileSet, f *ast.File, filename st return fixes, nil } - otherFiles := parseOtherFiles(fset, srcDir, filename) + otherFiles, err := parseOtherFiles(ctx, fset, srcDir, filename) + if err != nil { + return nil, err + } // Second pass: add information from other files in the same package, // like their package vars and imports. @@ -1192,7 +1196,7 @@ func addExternalCandidates(ctx context.Context, pass *pass, refs references, fil if err != nil { return err } - if err = resolver.scan(context.Background(), callback); err != nil { + if err = resolver.scan(ctx, callback); err != nil { return err } @@ -1203,7 +1207,7 @@ func addExternalCandidates(ctx context.Context, pass *pass, refs references, fil } results := make(chan result, len(refs)) - ctx, cancel := context.WithCancel(context.TODO()) + ctx, cancel := context.WithCancel(ctx) var wg sync.WaitGroup defer func() { cancel() diff --git a/vendor/modules.txt b/vendor/modules.txt index c5d5f511..8054ddcb 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -352,6 +352,11 @@ github.com/yuin/goldmark-emoji/definition # go.uber.org/atomic v1.9.0 ## explicit; go 1.13 go.uber.org/atomic +# go.uber.org/mock v0.4.0 +## explicit; go 1.20 +go.uber.org/mock/gomock +go.uber.org/mock/mockgen +go.uber.org/mock/mockgen/model # go.uber.org/multierr v1.9.0 ## explicit; go 1.19 go.uber.org/multierr @@ -363,12 +368,13 @@ golang.org/x/exp/slices golang.org/x/exp/slog golang.org/x/exp/slog/internal golang.org/x/exp/slog/internal/buffer -# golang.org/x/mod v0.17.0 +# golang.org/x/mod v0.18.0 ## explicit; go 1.18 golang.org/x/mod/internal/lazyregexp +golang.org/x/mod/modfile golang.org/x/mod/module golang.org/x/mod/semver -# golang.org/x/net v0.25.0 +# golang.org/x/net v0.26.0 ## explicit; go 1.18 golang.org/x/net/context golang.org/x/net/html @@ -381,15 +387,15 @@ golang.org/x/oauth2/internal ## explicit; go 1.18 golang.org/x/sync/errgroup golang.org/x/sync/singleflight -# golang.org/x/sys v0.20.0 +# golang.org/x/sys v0.21.0 ## explicit; go 1.18 golang.org/x/sys/plan9 golang.org/x/sys/unix golang.org/x/sys/windows -# golang.org/x/term v0.20.0 +# golang.org/x/term v0.21.0 ## explicit; go 1.18 golang.org/x/term -# golang.org/x/text v0.15.0 +# golang.org/x/text v0.16.0 ## explicit; go 1.18 golang.org/x/text/cases golang.org/x/text/encoding @@ -405,7 +411,7 @@ golang.org/x/text/language golang.org/x/text/runes golang.org/x/text/transform golang.org/x/text/unicode/norm -# golang.org/x/tools v0.21.0 +# golang.org/x/tools v0.22.0 ## explicit; go 1.19 golang.org/x/tools/go/ast/astutil golang.org/x/tools/imports From cf8efb929ab4c5e7e789c81fe915ac2efb617a35 Mon Sep 17 00:00:00 2001 From: Kelly Hofmann <55991524+k3llymariee@users.noreply.github.com> Date: Wed, 3 Jul 2024 14:12:49 -0700 Subject: [PATCH 54/79] [sc-248990] check if flag exists before adding override (#349) * check if flag exists before adding override * fix error handling * fix imports --- internal/dev_server/api/api.yaml | 20 +++++++++++++++++- internal/dev_server/api/server.gen.go | 28 +++++++++++++++++++++++++ internal/dev_server/api/server.go | 8 +++++++ internal/dev_server/model/override.go | 30 +++++++++++++++++++++------ internal/dev_server/model/store.go | 20 ++++++++++++++++++ 5 files changed, 99 insertions(+), 7 deletions(-) diff --git a/internal/dev_server/api/api.yaml b/internal/dev_server/api/api.yaml index 4b3ed202..7a5172c5 100644 --- a/internal/dev_server/api/api.yaml +++ b/internal/dev_server/api/api.yaml @@ -131,6 +131,8 @@ paths: responses: 200: $ref: "#/components/responses/FlagOverride" + 400: + $ref: "#/components/responses/InvalidRequestResponse" delete: summary: remove override for flag @@ -237,6 +239,22 @@ components: application/json: schema: $ref: "#/components/schemas/Project" + InvalidRequestResponse: + description: Invalid request response + content: + application/json: + schema: + type: object + required: + - code + - message + properties: + code: + type: string + description: specific error code encountered + message: + type: string + description: description of the error NotFoundErrorResp: description: not found content: @@ -250,4 +268,4 @@ components: code: type: string message: - type: string + type: string \ No newline at end of file diff --git a/internal/dev_server/api/server.gen.go b/internal/dev_server/api/server.gen.go index 6afbb3e1..35988e86 100644 --- a/internal/dev_server/api/server.gen.go +++ b/internal/dev_server/api/server.gen.go @@ -79,6 +79,15 @@ type FlagOverride struct { Value FlagValue `json:"value"` } +// InvalidRequestResponse defines model for InvalidRequestResponse. +type InvalidRequestResponse struct { + // Code specific error code encountered + Code string `json:"code"` + + // Message description of the error + Message string `json:"message"` +} + // NotFoundErrorResp defines model for NotFoundErrorResp. type NotFoundErrorResp struct { Code string `json:"code"` @@ -584,6 +593,14 @@ type FlagOverrideJSONResponse struct { Value FlagValue `json:"value"` } +type InvalidRequestResponseJSONResponse struct { + // Code specific error code encountered + Code string `json:"code"` + + // Message description of the error + Message string `json:"message"` +} + type NotFoundErrorRespJSONResponse struct { Code string `json:"code"` Message string `json:"message"` @@ -748,6 +765,17 @@ func (response PutDevProjectsProjectKeyOverridesFlagKey200JSONResponse) VisitPut return json.NewEncoder(w).Encode(response) } +type PutDevProjectsProjectKeyOverridesFlagKey400JSONResponse struct { + InvalidRequestResponseJSONResponse +} + +func (response PutDevProjectsProjectKeyOverridesFlagKey400JSONResponse) VisitPutDevProjectsProjectKeyOverridesFlagKeyResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(400) + + return json.NewEncoder(w).Encode(response) +} + type PatchDevProjectsProjectKeySyncRequestObject struct { ProjectKey ProjectKey `json:"projectKey"` Params PatchDevProjectsProjectKeySyncParams diff --git a/internal/dev_server/api/server.go b/internal/dev_server/api/server.go index 76850917..cdaf80d9 100644 --- a/internal/dev_server/api/server.go +++ b/internal/dev_server/api/server.go @@ -237,6 +237,14 @@ func (s Server) PutDevProjectsProjectKeyOverridesFlagKey(ctx context.Context, re } override, err := model.UpsertOverride(ctx, request.ProjectKey, request.FlagKey, *request.Body) if err != nil { + if errors.As(err, &model.Error{}) { + return PutDevProjectsProjectKeyOverridesFlagKey400JSONResponse{ + InvalidRequestResponseJSONResponse{ + Code: "invalid_request", + Message: err.Error(), + }, + }, nil + } return nil, err } return PutDevProjectsProjectKeyOverridesFlagKey200JSONResponse{FlagOverrideJSONResponse{ diff --git a/internal/dev_server/model/override.go b/internal/dev_server/model/override.go index 523963b7..435cb159 100644 --- a/internal/dev_server/model/override.go +++ b/internal/dev_server/model/override.go @@ -3,8 +3,9 @@ package model import ( "context" - "github.com/launchdarkly/go-sdk-common/v3/ldvalue" "github.com/pkg/errors" + + "github.com/launchdarkly/go-sdk-common/v3/ldvalue" ) type Override struct { @@ -22,7 +23,25 @@ type UpsertOverrideEvent struct { } func UpsertOverride(ctx context.Context, projectKey, flagKey string, value ldvalue.Value) (Override, error) { - // TODO: validate flag exists within project, + if the flag type matches + // TODO: validate if the flag type matches + + store := StoreFromContext(ctx) + + project, err := store.GetDevProject(ctx, projectKey) + if err != nil || project == nil { + return Override{}, NewError("project does not exist within dev server") + } + + var flagExists bool + for flag, _ := range project.FlagState { + if flagKey == flag { + flagExists = true + break + } + } + if !flagExists { + return Override{}, NewError("flag does not exist within dev project") + } override := Override{ ProjectKey: projectKey, @@ -31,14 +50,13 @@ func UpsertOverride(ctx context.Context, projectKey, flagKey string, value ldval Active: true, Version: 1, } - store := StoreFromContext(ctx) - override, err := store.UpsertOverride(ctx, override) + + override, err = store.UpsertOverride(ctx, override) if err != nil { return Override{}, err } observers := GetObserversFromContext(ctx) - project, err := store.GetDevProject(ctx, projectKey) if err != nil { return Override{}, errors.Wrap(err, "unable to get project") } @@ -49,7 +67,7 @@ func UpsertOverride(ctx context.Context, projectKey, flagKey string, value ldval FlagState: flagState, }) - return override, err + return override, nil } func (o Override) Apply(state FlagState) FlagState { diff --git a/internal/dev_server/model/store.go b/internal/dev_server/model/store.go index 09ab378d..534b9cc3 100644 --- a/internal/dev_server/model/store.go +++ b/internal/dev_server/model/store.go @@ -44,3 +44,23 @@ func StoreMiddleware(store Store) mux.MiddlewareFunc { } var ErrNotFound = errors.New("not found") + +type Error struct { + err error + message string +} + +func (e Error) Error() string { + return e.message +} + +func (e Error) Unwrap() error { + return e.err +} + +func NewError(message string) error { + return errors.WithStack(Error{ + err: errors.New(message), + message: message, + }) +} From 796c61aee540474a26eceaa8929e2b122bb800c9 Mon Sep 17 00:00:00 2001 From: Mike Zorn Date: Wed, 3 Jul 2024 14:14:10 -0700 Subject: [PATCH 55/79] upgrade go version to 1.22. This ended up being needed because a dependency used a standard library package which became available in 1.22. --- .github/workflows/go.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 609e4ab5..a7187638 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -19,7 +19,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v4 with: - go-version: '1.20' + go-version: '1.22' - name: Enforce formatting / go mod tidy run: | From 27222c0d7d395032184f21dcd9906245b615c79a Mon Sep 17 00:00:00 2001 From: Mike Zorn Date: Mon, 8 Jul 2024 05:16:29 -0700 Subject: [PATCH 56/79] fix: use sync map for observers (#370) * Remove WIP comments with API ideas No longer relevant * add failing unit test for deregister issue * use sync map --- internal/dev_server/api/api.yaml | 19 -------------- internal/dev_server/model/observer.go | 29 ++++++++++------------ internal/dev_server/model/observer_test.go | 24 +++++++++++++++++- 3 files changed, 36 insertions(+), 36 deletions(-) diff --git a/internal/dev_server/api/api.yaml b/internal/dev_server/api/api.yaml index 7a5172c5..3471095d 100644 --- a/internal/dev_server/api/api.yaml +++ b/internal/dev_server/api/api.yaml @@ -96,25 +96,6 @@ paths: responses: 201: $ref: "#/components/responses/Project" - # WIP updates can be added later - # patch: - # summary: change configuration of the project - # parameters: - # - $ref: '#/components/parameters/projectKey' - # responses: - - # WIP NOT SURE IF NEEDED TBH - # /dev/projects/{projectKey}/overrides: - # get: - # summary: return all the flag overrides for the project - # parameters: - # - $ref: '#/components/parameters/projectKey' - # responses: // WIP - # delete: - # summary: clear all overrides for the project - # parameters: - # - $ref: '#/components/parameters/projectKey' - # responses: // WIP /dev/projects/{projectKey}/overrides/{flagKey}: put: summary: override flag value with value provided in the body diff --git a/internal/dev_server/model/observer.go b/internal/dev_server/model/observer.go index 480a81d1..05d2128a 100644 --- a/internal/dev_server/model/observer.go +++ b/internal/dev_server/model/observer.go @@ -2,6 +2,7 @@ package model import ( "log" + "sync" "github.com/google/uuid" ) @@ -13,36 +14,32 @@ type Observer interface { } type Observers struct { - observers map[uuid.UUID]Observer + observers sync.Map } func NewObservers() *Observers { observers := new(Observers) - observers.observers = make(map[uuid.UUID]Observer) + observers.observers = sync.Map{} return observers } func (o *Observers) DeregisterObserver(observerId uuid.UUID) bool { - log.Printf("DeregisterObserver: observer %+v", observerId) - for key := range o.observers { - if key == observerId { - delete(o.observers, key) - return true - } - } - return false + log.Printf("DeregisterObserver: observerId %+v", observerId) + _, exists := o.observers.LoadAndDelete(observerId) + return exists } func (o *Observers) RegisterObserver(observer Observer) uuid.UUID { - log.Printf("RegisterObserver: observer %+v", observer) id := uuid.New() - o.observers[id] = observer + log.Printf("RegisterObserver: observer %+v, id %s", observer, id) + o.observers.Store(id, observer) return id } func (o *Observers) Notify(event interface{}) { - log.Printf("Notify: event %+v to %d observers", event, len(o.observers)) - for _, observer := range o.observers { - observer.Handle(event) - } + log.Printf("Notify: event %+v to observers", event) + o.observers.Range(func(_, observer any) bool { + observer.(Observer).Handle(event) + return true + }) } diff --git a/internal/dev_server/model/observer_test.go b/internal/dev_server/model/observer_test.go index c879c620..8380f989 100644 --- a/internal/dev_server/model/observer_test.go +++ b/internal/dev_server/model/observer_test.go @@ -1,8 +1,10 @@ package model import ( + "sync" "testing" + "github.com/google/uuid" "github.com/stretchr/testify/assert" ) @@ -33,5 +35,25 @@ func TestObservers(t *testing.T) { assert.True(t, ok, "observer should be deregistered") observers.Notify("lol") }) - + t.Run("deregistering from multiple go routines should not panic", func(t *testing.T) { + observers := NewObservers() + observer := testObserver{handle: func(i interface{}) { + assert.Fail(t, "should not be called") + }} + ids := make([]uuid.UUID, 100) + for i := 0; i < 100; i++ { + i := i + ids[i] = observers.RegisterObserver(observer) + } + wg := sync.WaitGroup{} + for _, id := range ids { + id := id + wg.Add(1) + go func() { + observers.DeregisterObserver(id) + wg.Done() + }() + } + wg.Wait() + }) } From ddf4190823dd480e629327336d93522113622d36 Mon Sep 17 00:00:00 2001 From: Thomas Varney <70972175+tvarney13@users.noreply.github.com> Date: Mon, 8 Jul 2024 09:21:51 -0400 Subject: [PATCH 57/79] test: Improve Test Coverage of model package (#367) * add store mock and bundle mocks together in same directory * override.go tests * new test file and put observer_test in test package * more tests * Final project tests * fix nit and tests, add more coverage * PR suggestions --- internal/dev_server/adapters/mocks/utils.go | 17 ++ internal/dev_server/model/flags_state_test.go | 45 +++ .../{mock_observer => mocks}/observer.go | 6 +- internal/dev_server/model/mocks/store.go | 159 ++++++++++ internal/dev_server/model/observer.go | 2 +- internal/dev_server/model/observer_test.go | 9 +- internal/dev_server/model/override.go | 7 +- internal/dev_server/model/override_test.go | 121 ++++++++ internal/dev_server/model/project.go | 19 +- internal/dev_server/model/project_test.go | 275 ++++++++++++++++++ internal/dev_server/model/store.go | 2 + internal/dev_server/sdk/go_sdk_test.go | 6 +- 12 files changed, 642 insertions(+), 26 deletions(-) create mode 100644 internal/dev_server/adapters/mocks/utils.go create mode 100644 internal/dev_server/model/flags_state_test.go rename internal/dev_server/model/{mock_observer => mocks}/observer.go (88%) create mode 100644 internal/dev_server/model/mocks/store.go create mode 100644 internal/dev_server/model/override_test.go create mode 100644 internal/dev_server/model/project_test.go diff --git a/internal/dev_server/adapters/mocks/utils.go b/internal/dev_server/adapters/mocks/utils.go new file mode 100644 index 00000000..4cf012b6 --- /dev/null +++ b/internal/dev_server/adapters/mocks/utils.go @@ -0,0 +1,17 @@ +package mocks + +import ( + "context" + + "github.com/launchdarkly/ldcli/internal/dev_server/adapters" + "go.uber.org/mock/gomock" +) + +func WithMockApiAndSdk(ctx context.Context, controller *gomock.Controller) (context.Context, *MockApi, *MockSdk) { + api := NewMockApi(controller) + ctx = adapters.WithApi(ctx, api) + sdk := NewMockSdk(controller) + ctx = adapters.WithSdk(ctx, sdk) + + return ctx, api, sdk +} diff --git a/internal/dev_server/model/flags_state_test.go b/internal/dev_server/model/flags_state_test.go new file mode 100644 index 00000000..e79d6c9e --- /dev/null +++ b/internal/dev_server/model/flags_state_test.go @@ -0,0 +1,45 @@ +package model_test + +import ( + "testing" + + "github.com/launchdarkly/go-sdk-common/v3/ldvalue" + "github.com/launchdarkly/go-server-sdk/v7/interfaces/flagstate" + "github.com/launchdarkly/ldcli/internal/dev_server/model" + "github.com/stretchr/testify/assert" +) + +func TestFromAllFlags(t *testing.T) { + sdkFlags := flagstate.NewAllFlagsBuilder(). + AddFlag("boolFlag", flagstate.FlagState{Value: ldvalue.Bool(true), Version: 1}). + AddFlag("stringFlag", flagstate.FlagState{Value: ldvalue.String("cool"), Version: 1}). + AddFlag("intFlag", flagstate.FlagState{Value: ldvalue.Int(123), Version: 1}). + AddFlag("doubleFlag", flagstate.FlagState{Value: ldvalue.Float64(99.99), Version: 1}). + AddFlag("jsonFlag", flagstate.FlagState{Value: ldvalue.CopyArbitraryValue(map[string]any{"cat": "hat"}), Version: 1}). + Build() + + flagState := model.FromAllFlags(sdkFlags) + expectedVersion := 1 + + for key, state := range flagState { + var expectedVal ldvalue.Value + + switch key { + case "boolFlag": + expectedVal = ldvalue.Bool(true) + case "stringFlag": + expectedVal = ldvalue.String("cool") + case "intFlag": + expectedVal = ldvalue.Int(123) + case "doubleFlag": + expectedVal = ldvalue.Float64(99.99) + case "jsonFlag": + expectedVal = ldvalue.CopyArbitraryValue(map[string]any{"cat": "hat"}) + default: + assert.Failf(t, "unknown flag, %s", key) + } + + assert.Equal(t, expectedVersion, state.Version) + assert.True(t, expectedVal.Equal(state.Value)) + } +} diff --git a/internal/dev_server/model/mock_observer/observer.go b/internal/dev_server/model/mocks/observer.go similarity index 88% rename from internal/dev_server/model/mock_observer/observer.go rename to internal/dev_server/model/mocks/observer.go index 62c67fcf..5c4b96bd 100644 --- a/internal/dev_server/model/mock_observer/observer.go +++ b/internal/dev_server/model/mocks/observer.go @@ -3,11 +3,11 @@ // // Generated by this command: // -// mockgen -destination mock_observer/observer.go -package mock_observer . Observer +// mockgen -destination mocks/observer.go -package mocks . Observer // -// Package mock_observer is a generated GoMock package. -package mock_observer +// Package mocks is a generated GoMock package. +package mocks import ( reflect "reflect" diff --git a/internal/dev_server/model/mocks/store.go b/internal/dev_server/model/mocks/store.go new file mode 100644 index 00000000..210c0d14 --- /dev/null +++ b/internal/dev_server/model/mocks/store.go @@ -0,0 +1,159 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/launchdarkly/ldcli/internal/dev_server/model (interfaces: Store) +// +// Generated by this command: +// +// mockgen -destination mocks/store.go -package mocks . Store +// + +// Package mocks is a generated GoMock package. +package mocks + +import ( + context "context" + reflect "reflect" + + model "github.com/launchdarkly/ldcli/internal/dev_server/model" + gomock "go.uber.org/mock/gomock" +) + +// MockStore is a mock of Store interface. +type MockStore struct { + ctrl *gomock.Controller + recorder *MockStoreMockRecorder +} + +// MockStoreMockRecorder is the mock recorder for MockStore. +type MockStoreMockRecorder struct { + mock *MockStore +} + +// NewMockStore creates a new mock instance. +func NewMockStore(ctrl *gomock.Controller) *MockStore { + mock := &MockStore{ctrl: ctrl} + mock.recorder = &MockStoreMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockStore) EXPECT() *MockStoreMockRecorder { + return m.recorder +} + +// DeleteDevProject mocks base method. +func (m *MockStore) DeleteDevProject(arg0 context.Context, arg1 string) (bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteDevProject", arg0, arg1) + ret0, _ := ret[0].(bool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// DeleteDevProject indicates an expected call of DeleteDevProject. +func (mr *MockStoreMockRecorder) DeleteDevProject(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteDevProject", reflect.TypeOf((*MockStore)(nil).DeleteDevProject), arg0, arg1) +} + +// DeleteOverride mocks base method. +func (m *MockStore) DeleteOverride(arg0 context.Context, arg1, arg2 string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteOverride", arg0, arg1, arg2) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteOverride indicates an expected call of DeleteOverride. +func (mr *MockStoreMockRecorder) DeleteOverride(arg0, arg1, arg2 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteOverride", reflect.TypeOf((*MockStore)(nil).DeleteOverride), arg0, arg1, arg2) +} + +// GetDevProject mocks base method. +func (m *MockStore) GetDevProject(arg0 context.Context, arg1 string) (*model.Project, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetDevProject", arg0, arg1) + ret0, _ := ret[0].(*model.Project) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetDevProject indicates an expected call of GetDevProject. +func (mr *MockStoreMockRecorder) GetDevProject(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDevProject", reflect.TypeOf((*MockStore)(nil).GetDevProject), arg0, arg1) +} + +// GetDevProjects mocks base method. +func (m *MockStore) GetDevProjects(arg0 context.Context) ([]string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetDevProjects", arg0) + ret0, _ := ret[0].([]string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetDevProjects indicates an expected call of GetDevProjects. +func (mr *MockStoreMockRecorder) GetDevProjects(arg0 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDevProjects", reflect.TypeOf((*MockStore)(nil).GetDevProjects), arg0) +} + +// GetOverridesForProject mocks base method. +func (m *MockStore) GetOverridesForProject(arg0 context.Context, arg1 string) (model.Overrides, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetOverridesForProject", arg0, arg1) + ret0, _ := ret[0].(model.Overrides) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetOverridesForProject indicates an expected call of GetOverridesForProject. +func (mr *MockStoreMockRecorder) GetOverridesForProject(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetOverridesForProject", reflect.TypeOf((*MockStore)(nil).GetOverridesForProject), arg0, arg1) +} + +// InsertProject mocks base method. +func (m *MockStore) InsertProject(arg0 context.Context, arg1 model.Project) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "InsertProject", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// InsertProject indicates an expected call of InsertProject. +func (mr *MockStoreMockRecorder) InsertProject(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertProject", reflect.TypeOf((*MockStore)(nil).InsertProject), arg0, arg1) +} + +// UpdateProject mocks base method. +func (m *MockStore) UpdateProject(arg0 context.Context, arg1 model.Project) (bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateProject", arg0, arg1) + ret0, _ := ret[0].(bool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// UpdateProject indicates an expected call of UpdateProject. +func (mr *MockStoreMockRecorder) UpdateProject(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateProject", reflect.TypeOf((*MockStore)(nil).UpdateProject), arg0, arg1) +} + +// UpsertOverride mocks base method. +func (m *MockStore) UpsertOverride(arg0 context.Context, arg1 model.Override) (model.Override, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpsertOverride", arg0, arg1) + ret0, _ := ret[0].(model.Override) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// UpsertOverride indicates an expected call of UpsertOverride. +func (mr *MockStoreMockRecorder) UpsertOverride(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertOverride", reflect.TypeOf((*MockStore)(nil).UpsertOverride), arg0, arg1) +} diff --git a/internal/dev_server/model/observer.go b/internal/dev_server/model/observer.go index 05d2128a..e66c6290 100644 --- a/internal/dev_server/model/observer.go +++ b/internal/dev_server/model/observer.go @@ -7,7 +7,7 @@ import ( "github.com/google/uuid" ) -//go:generate go run go.uber.org/mock/mockgen -destination mock_observer/observer.go -package mock_observer . Observer +//go:generate go run go.uber.org/mock/mockgen -destination mocks/observer.go -package mocks . Observer type Observer interface { Handle(interface{}) diff --git a/internal/dev_server/model/observer_test.go b/internal/dev_server/model/observer_test.go index 8380f989..7a5cef91 100644 --- a/internal/dev_server/model/observer_test.go +++ b/internal/dev_server/model/observer_test.go @@ -1,10 +1,11 @@ -package model +package model_test import ( "sync" "testing" "github.com/google/uuid" + "github.com/launchdarkly/ldcli/internal/dev_server/model" "github.com/stretchr/testify/assert" ) @@ -16,7 +17,7 @@ func (o testObserver) Handle(event interface{}) { o.handle(event) } func TestObservers(t *testing.T) { t.Run("Register then notify yields notification", func(t *testing.T) { - observers := NewObservers() + observers := model.NewObservers() observerCalled := false observer := testObserver{handle: func(i interface{}) { observerCalled = true @@ -26,7 +27,7 @@ func TestObservers(t *testing.T) { assert.True(t, observerCalled, "observer should be called") }) t.Run("Register, deregister then notify yields NO notification", func(t *testing.T) { - observers := NewObservers() + observers := model.NewObservers() observer := testObserver{handle: func(i interface{}) { assert.Fail(t, "should not be called") }} @@ -36,7 +37,7 @@ func TestObservers(t *testing.T) { observers.Notify("lol") }) t.Run("deregistering from multiple go routines should not panic", func(t *testing.T) { - observers := NewObservers() + observers := model.NewObservers() observer := testObserver{handle: func(i interface{}) { assert.Fail(t, "should not be called") }} diff --git a/internal/dev_server/model/override.go b/internal/dev_server/model/override.go index 435cb159..a86f6513 100644 --- a/internal/dev_server/model/override.go +++ b/internal/dev_server/model/override.go @@ -3,8 +3,6 @@ package model import ( "context" - "github.com/pkg/errors" - "github.com/launchdarkly/go-sdk-common/v3/ldvalue" ) @@ -33,7 +31,7 @@ func UpsertOverride(ctx context.Context, projectKey, flagKey string, value ldval } var flagExists bool - for flag, _ := range project.FlagState { + for flag := range project.FlagState { if flagKey == flag { flagExists = true break @@ -57,9 +55,6 @@ func UpsertOverride(ctx context.Context, projectKey, flagKey string, value ldval } observers := GetObserversFromContext(ctx) - if err != nil { - return Override{}, errors.Wrap(err, "unable to get project") - } flagState := override.Apply(project.FlagState[flagKey]) observers.Notify(UpsertOverrideEvent{ FlagKey: flagKey, diff --git a/internal/dev_server/model/override_test.go b/internal/dev_server/model/override_test.go new file mode 100644 index 00000000..0ef4c79b --- /dev/null +++ b/internal/dev_server/model/override_test.go @@ -0,0 +1,121 @@ +package model_test + +import ( + "context" + "errors" + "testing" + + "github.com/launchdarkly/go-sdk-common/v3/ldvalue" + "github.com/launchdarkly/ldcli/internal/dev_server/model" + "github.com/launchdarkly/ldcli/internal/dev_server/model/mocks" + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" +) + +func TestUpsertOverride(t *testing.T) { + ctx := context.Background() + mockController := gomock.NewController(t) + store := mocks.NewMockStore(mockController) + projKey := "proj" + flagKey := "flg" + ldValue := ldvalue.Bool(true) + override := model.Override{ + ProjectKey: projKey, + FlagKey: flagKey, + Value: ldValue, + Active: true, + Version: 1, + } + + project := &model.Project{ + Key: projKey, + FlagState: model.FlagsState{flagKey: model.FlagState{Value: ldvalue.Bool(false), Version: 1}}, + } + + ctx = model.ContextWithStore(ctx, store) + + observers := model.NewObservers() + observer := mocks.NewMockObserver(mockController) + + observers.RegisterObserver(observer) + ctx = model.SetObserversOnContext(ctx, observers) + + t.Run("store unable to get project, returns error", func(t *testing.T) { + store.EXPECT().GetDevProject(gomock.Any(), projKey).Return(nil, errors.New("test 2")) + + _, err := model.UpsertOverride(ctx, projKey, flagKey, ldValue) + assert.Error(t, err) + assert.Contains(t, err.Error(), "project does not exist within dev server") + }) + + t.Run("Returns error if flag does not exist in project", func(t *testing.T) { + badProj := model.Project{ + Key: projKey, + FlagState: model.FlagsState{}, + } + store.EXPECT().GetDevProject(gomock.Any(), projKey).Return(&badProj, nil) + + _, err := model.UpsertOverride(ctx, projKey, flagKey, ldValue) + assert.Error(t, err) + assert.Contains(t, err.Error(), "flag does not exist within dev project") + }) + + t.Run("store fails to upsert, returns error", func(t *testing.T) { + store.EXPECT().GetDevProject(gomock.Any(), projKey).Return(project, nil) + store.EXPECT().UpsertOverride(gomock.Any(), gomock.Any()).Return(model.Override{}, errors.New("testy test")) + + _, err := model.UpsertOverride(ctx, projKey, flagKey, ldValue) + assert.Error(t, err) + assert.Equal(t, "testy test", err.Error()) + }) + + t.Run("override is applied, observers are notified", func(t *testing.T) { + store.EXPECT().GetDevProject(gomock.Any(), projKey).Return(project, nil) + store.EXPECT().UpsertOverride(gomock.Any(), override).Return(override, nil) + observer. + EXPECT(). + Handle(model.UpsertOverrideEvent{ + FlagKey: flagKey, + ProjectKey: projKey, + FlagState: model.FlagState{Value: ldvalue.Bool(true), Version: 2}, + }) + + o, err := model.UpsertOverride(ctx, projKey, flagKey, ldValue) + assert.Nil(t, err) + assert.Equal(t, override, o) + }) +} + +func TestOverrideApply(t *testing.T) { + projKey := "proj" + flagKey := "flg" + ldValue := ldvalue.Bool(true) + oldState := model.FlagState{Value: ldvalue.Bool(false), Version: 1} + + t.Run("if override is inactive, increment version", func(t *testing.T) { + override := model.Override{ + ProjectKey: projKey, + FlagKey: flagKey, + Value: ldValue, + Version: 1, + } + + state := override.Apply(oldState) + assert.False(t, state.Value.BoolValue()) + assert.Equal(t, 2, state.Version) + }) + + t.Run("if override is active, increment version AND update value", func(t *testing.T) { + override := model.Override{ + ProjectKey: projKey, + FlagKey: flagKey, + Value: ldValue, + Active: true, + Version: 1, + } + + state := override.Apply(oldState) + assert.True(t, state.Value.BoolValue()) + assert.Equal(t, 2, state.Version) + }) +} diff --git a/internal/dev_server/model/project.go b/internal/dev_server/model/project.go index 9562cbb9..5cc0051c 100644 --- a/internal/dev_server/model/project.go +++ b/internal/dev_server/model/project.go @@ -19,15 +19,17 @@ type Project struct { // CreateProject creates a project and adds it to the database. func CreateProject(ctx context.Context, projectKey, sourceEnvironmentKey string, ldCtx *ldcontext.Context) (Project, error) { - store := StoreFromContext(ctx) - project := Project{} - project.Key = projectKey - project.SourceEnvironmentKey = sourceEnvironmentKey + project := Project{ + Key: projectKey, + SourceEnvironmentKey: sourceEnvironmentKey, + } + if ldCtx == nil { project.Context = ldcontext.NewBuilder("user").Key("dev-environment").Build() } else { project.Context = *ldCtx } + flagsState, err := project.FetchFlagState(ctx) if err != nil { return Project{}, err @@ -36,6 +38,7 @@ func CreateProject(ctx context.Context, projectKey, sourceEnvironmentKey string, project.FlagState = flagsState project.LastSyncTime = time.Now() + store := StoreFromContext(ctx) err = store.InsertProject(ctx, project) if err != nil { return Project{}, err @@ -71,7 +74,7 @@ func UpdateProject(ctx context.Context, projectKey string, context *ldcontext.Co return Project{}, err } if !updated { - return Project{}, err + return Project{}, errors.New("Project not updated") } return *project, nil } @@ -94,7 +97,7 @@ func SyncProject(ctx context.Context, projectKey string) (Project, error) { return Project{}, err } if !updated { - return Project{}, err + return Project{}, errors.New("Project not updated") } return *project, nil } @@ -116,17 +119,19 @@ func (p Project) GetFlagStateWithOverridesForProject(ctx context.Context) (Flags } func (p Project) FetchFlagState(ctx context.Context) (FlagsState, error) { - sdkAdapter := adapters.GetSdk(ctx) apiAdapter := adapters.GetApi(ctx) sdkKey, err := apiAdapter.GetSdkKey(ctx, p.Key, p.SourceEnvironmentKey) flagsState := make(FlagsState) if err != nil { return flagsState, err } + + sdkAdapter := adapters.GetSdk(ctx) sdkFlags, err := sdkAdapter.GetAllFlagsState(ctx, p.Context, sdkKey) if err != nil { return flagsState, err } + flagsState = FromAllFlags(sdkFlags) return flagsState, nil } diff --git a/internal/dev_server/model/project_test.go b/internal/dev_server/model/project_test.go new file mode 100644 index 00000000..48d33b27 --- /dev/null +++ b/internal/dev_server/model/project_test.go @@ -0,0 +1,275 @@ +package model_test + +import ( + "context" + "errors" + "testing" + + "github.com/launchdarkly/go-sdk-common/v3/ldcontext" + "github.com/launchdarkly/go-sdk-common/v3/ldvalue" + "github.com/launchdarkly/go-server-sdk/v7/interfaces/flagstate" + adapters_mocks "github.com/launchdarkly/ldcli/internal/dev_server/adapters/mocks" + "github.com/launchdarkly/ldcli/internal/dev_server/model" + "github.com/launchdarkly/ldcli/internal/dev_server/model/mocks" + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" +) + +func TestCreateProject(t *testing.T) { + ctx := context.Background() + mockController := gomock.NewController(t) + ctx, api, sdk := adapters_mocks.WithMockApiAndSdk(ctx, mockController) + store := mocks.NewMockStore(mockController) + ctx = model.ContextWithStore(ctx, store) + projKey := "proj" + sourceEnvKey := "env" + sdkKey := "thing" + + allFlags := flagstate.NewAllFlagsBuilder(). + AddFlag("boolFlag", flagstate.FlagState{Value: ldvalue.Bool(true)}). + Build() + + t.Run("Returns error if it cant fetch flag state", func(t *testing.T) { + api.EXPECT().GetSdkKey(gomock.Any(), projKey, sourceEnvKey).Return("", errors.New("fetch flag state fails")) + _, err := model.CreateProject(ctx, projKey, sourceEnvKey, nil) + assert.NotNil(t, err) + assert.Equal(t, "fetch flag state fails", err.Error()) + }) + + t.Run("Returns error if it fails to insert the project", func(t *testing.T) { + api.EXPECT().GetSdkKey(gomock.Any(), projKey, sourceEnvKey).Return(sdkKey, nil) + sdk.EXPECT().GetAllFlagsState(gomock.Any(), gomock.Any(), sdkKey).Return(allFlags, nil) + store.EXPECT().InsertProject(gomock.Any(), gomock.Any()).Return(errors.New("insert fails")) + + _, err := model.CreateProject(ctx, projKey, sourceEnvKey, nil) + assert.NotNil(t, err) + assert.Equal(t, "insert fails", err.Error()) + }) + + t.Run("Successfully creates project", func(t *testing.T) { + api.EXPECT().GetSdkKey(gomock.Any(), projKey, sourceEnvKey).Return(sdkKey, nil) + sdk.EXPECT().GetAllFlagsState(gomock.Any(), gomock.Any(), sdkKey).Return(allFlags, nil) + store.EXPECT().InsertProject(gomock.Any(), gomock.Any()).Return(nil) + + p, err := model.CreateProject(ctx, projKey, sourceEnvKey, nil) + assert.Nil(t, err) + + expectedProj := model.Project{ + Key: projKey, + SourceEnvironmentKey: sourceEnvKey, + Context: ldcontext.NewBuilder("user").Key("dev-environment").Build(), + FlagState: model.FromAllFlags(allFlags), + } + + assert.Equal(t, expectedProj.Key, p.Key) + assert.Equal(t, expectedProj.SourceEnvironmentKey, p.SourceEnvironmentKey) + assert.Equal(t, expectedProj.Context, p.Context) + assert.Equal(t, expectedProj.FlagState, p.FlagState) + }) +} + +func TestUpdateProject(t *testing.T) { + mockController := gomock.NewController(t) + store := mocks.NewMockStore(mockController) + ctx := model.ContextWithStore(context.Background(), store) + ctx, api, sdk := adapters_mocks.WithMockApiAndSdk(ctx, mockController) + ldCtx := ldcontext.New(t.Name()) + newSrcEnv := "newEnv" + + proj := model.Project{ + Key: "projKey", + SourceEnvironmentKey: "srcEnvKey", + Context: ldcontext.New(t.Name()), + } + + t.Run("Returns error if GetDevProject fails", func(t *testing.T) { + store.EXPECT().GetDevProject(gomock.Any(), proj.Key).Return(&model.Project{}, errors.New("GetDevProject fails")) + _, err := model.UpdateProject(ctx, proj.Key, nil, nil) + assert.NotNil(t, err) + assert.Equal(t, "GetDevProject fails", err.Error()) + }) + + t.Run("Passing in context triggers FetchFlagState, returns error if the fetch fails", func(t *testing.T) { + store.EXPECT().GetDevProject(gomock.Any(), proj.Key).Return(&proj, nil) + api.EXPECT().GetSdkKey(gomock.Any(), proj.Key, proj.SourceEnvironmentKey).Return("", errors.New("FetchFlagState fails")) + + _, err := model.UpdateProject(ctx, proj.Key, &ldCtx, nil) + assert.NotNil(t, err) + assert.Equal(t, "FetchFlagState fails", err.Error()) + }) + + t.Run("Passing in sourceEnvironmentKey triggers FetchFlagState, returns error if UpdateProject fails", func(t *testing.T) { + store.EXPECT().GetDevProject(gomock.Any(), proj.Key).Return(&proj, nil) + api.EXPECT().GetSdkKey(gomock.Any(), proj.Key, newSrcEnv).Return("sdkKey", nil) + sdk.EXPECT().GetAllFlagsState(gomock.Any(), gomock.Any(), "sdkKey").Return(flagstate.AllFlags{}, nil) + store.EXPECT().UpdateProject(gomock.Any(), gomock.Any()).Return(false, errors.New("UpdateProject fails")) + + _, err := model.UpdateProject(ctx, proj.Key, nil, &newSrcEnv) + assert.NotNil(t, err) + assert.Equal(t, "UpdateProject fails", err.Error()) + }) + + t.Run("Returns error if project was not actually updated", func(t *testing.T) { + store.EXPECT().GetDevProject(gomock.Any(), proj.Key).Return(&proj, nil) + store.EXPECT().UpdateProject(gomock.Any(), gomock.Any()).Return(false, nil) + + _, err := model.UpdateProject(ctx, proj.Key, nil, nil) + assert.NotNil(t, err) + assert.Equal(t, "Project not updated", err.Error()) + }) + + t.Run("Return successfully", func(t *testing.T) { + store.EXPECT().GetDevProject(gomock.Any(), proj.Key).Return(&proj, nil) + store.EXPECT().UpdateProject(gomock.Any(), gomock.Any()).Return(true, nil) + + project, err := model.UpdateProject(ctx, proj.Key, nil, nil) + assert.Nil(t, err) + assert.Equal(t, proj, project) + }) +} + +func TestSyncProject(t *testing.T) { + mockController := gomock.NewController(t) + store := mocks.NewMockStore(mockController) + ctx := model.ContextWithStore(context.Background(), store) + ctx, api, sdk := adapters_mocks.WithMockApiAndSdk(ctx, mockController) + + proj := model.Project{ + Key: "projKey", + SourceEnvironmentKey: "srcEnvKey", + Context: ldcontext.New(t.Name()), + } + + t.Run("Returns error if GetDevProject fails", func(t *testing.T) { + store.EXPECT().GetDevProject(gomock.Any(), proj.Key).Return(&model.Project{}, errors.New("GetDevProject fails")) + _, err := model.SyncProject(ctx, proj.Key) + assert.NotNil(t, err) + assert.Equal(t, "GetDevProject fails", err.Error()) + }) + + t.Run("returns error if FetchFlagState fails", func(t *testing.T) { + store.EXPECT().GetDevProject(gomock.Any(), proj.Key).Return(&proj, nil) + api.EXPECT().GetSdkKey(gomock.Any(), proj.Key, proj.SourceEnvironmentKey).Return("", errors.New("FetchFlagState fails")) + + _, err := model.SyncProject(ctx, proj.Key) + assert.NotNil(t, err) + assert.Equal(t, "FetchFlagState fails", err.Error()) + }) + + t.Run("returns error if UpdateProject fails", func(t *testing.T) { + store.EXPECT().GetDevProject(gomock.Any(), proj.Key).Return(&proj, nil) + api.EXPECT().GetSdkKey(gomock.Any(), proj.Key, proj.SourceEnvironmentKey).Return("sdkKey", nil) + sdk.EXPECT().GetAllFlagsState(gomock.Any(), gomock.Any(), "sdkKey").Return(flagstate.AllFlags{}, nil) + store.EXPECT().UpdateProject(gomock.Any(), gomock.Any()).Return(false, errors.New("UpdateProject fails")) + + _, err := model.SyncProject(ctx, proj.Key) + assert.NotNil(t, err) + assert.Equal(t, "UpdateProject fails", err.Error()) + }) + + t.Run("Returns error if project was not actually updated", func(t *testing.T) { + store.EXPECT().GetDevProject(gomock.Any(), proj.Key).Return(&proj, nil) + api.EXPECT().GetSdkKey(gomock.Any(), proj.Key, proj.SourceEnvironmentKey).Return("sdkKey", nil) + sdk.EXPECT().GetAllFlagsState(gomock.Any(), gomock.Any(), "sdkKey").Return(flagstate.AllFlags{}, nil) + store.EXPECT().UpdateProject(gomock.Any(), gomock.Any()).Return(false, nil) + + _, err := model.SyncProject(ctx, proj.Key) + assert.NotNil(t, err) + assert.Equal(t, "Project not updated", err.Error()) + }) + + t.Run("Return successfully", func(t *testing.T) { + store.EXPECT().GetDevProject(gomock.Any(), proj.Key).Return(&proj, nil) + api.EXPECT().GetSdkKey(gomock.Any(), proj.Key, proj.SourceEnvironmentKey).Return("sdkKey", nil) + sdk.EXPECT().GetAllFlagsState(gomock.Any(), gomock.Any(), "sdkKey").Return(flagstate.AllFlags{}, nil) + store.EXPECT().UpdateProject(gomock.Any(), gomock.Any()).Return(true, nil) + + project, err := model.SyncProject(ctx, proj.Key) + assert.Nil(t, err) + assert.Equal(t, proj, project) + }) +} + +func TestGetFlagStateWithOverridesForProject(t *testing.T) { + mockController := gomock.NewController(t) + store := mocks.NewMockStore(mockController) + ctx := model.ContextWithStore(context.Background(), store) + flagKey := "flg" + proj := model.Project{ + Key: "projKey", + FlagState: model.FlagsState{flagKey: model.FlagState{Value: ldvalue.Bool(false), Version: 1}}, + } + + t.Run("Returns error if store fetch fails", func(t *testing.T) { + store.EXPECT().GetOverridesForProject(gomock.Any(), proj.Key).Return(model.Overrides{}, errors.New("fetch fails")) + + _, err := proj.GetFlagStateWithOverridesForProject(ctx) + assert.NotNil(t, err) + assert.Equal(t, "unable to fetch overrides for project projKey: fetch fails", err.Error()) + }) + + t.Run("Returns flag state with overrides successfully", func(t *testing.T) { + overrides := model.Overrides{ + { + ProjectKey: proj.Key, + FlagKey: flagKey, + Value: ldvalue.Bool(true), + Active: true, + Version: 1, + }, + } + + store.EXPECT().GetOverridesForProject(gomock.Any(), proj.Key).Return(overrides, nil) + + withOverrides, err := proj.GetFlagStateWithOverridesForProject(ctx) + assert.Nil(t, err) + + assert.Len(t, withOverrides, 1) + + overriddenFlag, exists := withOverrides[flagKey] + assert.True(t, exists) + assert.True(t, overriddenFlag.Value.BoolValue()) + assert.Equal(t, 2, overriddenFlag.Version) + }) +} + +func TestFetchFlagState(t *testing.T) { + ctx := context.Background() + mockController := gomock.NewController(t) + ctx, api, sdk := adapters_mocks.WithMockApiAndSdk(ctx, mockController) + allFlags := flagstate.NewAllFlagsBuilder(). + AddFlag("boolFlag", flagstate.FlagState{Value: ldvalue.Bool(true)}). + Build() + + proj := model.Project{ + Key: "projKey", + SourceEnvironmentKey: "srcEnvKey", + Context: ldcontext.New(t.Name()), + } + + t.Run("Returns error if fails to fetch sdk key", func(t *testing.T) { + api.EXPECT().GetSdkKey(gomock.Any(), proj.Key, proj.SourceEnvironmentKey).Return("", errors.New("sdk key fail")) + + _, err := proj.FetchFlagState(ctx) + assert.NotNil(t, err) + assert.Equal(t, "sdk key fail", err.Error()) + }) + + t.Run("Returns error if fails to fetch flags state", func(t *testing.T) { + api.EXPECT().GetSdkKey(gomock.Any(), proj.Key, proj.SourceEnvironmentKey).Return("key", nil) + sdk.EXPECT().GetAllFlagsState(gomock.Any(), gomock.Any(), "key").Return(flagstate.AllFlags{}, errors.New("fetch fail")) + + _, err := proj.FetchFlagState(ctx) + assert.NotNil(t, err) + assert.Equal(t, "fetch fail", err.Error()) + }) + + t.Run("Successfully fetches flag state", func(t *testing.T) { + api.EXPECT().GetSdkKey(gomock.Any(), proj.Key, proj.SourceEnvironmentKey).Return("key", nil) + sdk.EXPECT().GetAllFlagsState(gomock.Any(), gomock.Any(), "key").Return(allFlags, nil) + + flagState, err := proj.FetchFlagState(ctx) + assert.Nil(t, err) + assert.Equal(t, model.FromAllFlags(allFlags), flagState) + }) +} diff --git a/internal/dev_server/model/store.go b/internal/dev_server/model/store.go index 534b9cc3..1b6f27df 100644 --- a/internal/dev_server/model/store.go +++ b/internal/dev_server/model/store.go @@ -12,6 +12,8 @@ type ctxKey string const ctxKeyStore = ctxKey("model.Store") +//go:generate go run go.uber.org/mock/mockgen -destination mocks/store.go -package mocks . Store + type Store interface { DeleteOverride(ctx context.Context, projectKey, flagKey string) error GetDevProjects(ctx context.Context) ([]string, error) diff --git a/internal/dev_server/sdk/go_sdk_test.go b/internal/dev_server/sdk/go_sdk_test.go index a97e5c2c..ac4e5332 100644 --- a/internal/dev_server/sdk/go_sdk_test.go +++ b/internal/dev_server/sdk/go_sdk_test.go @@ -13,7 +13,6 @@ import ( "github.com/launchdarkly/go-sdk-common/v3/ldvalue" ldclient "github.com/launchdarkly/go-server-sdk/v7" "github.com/launchdarkly/go-server-sdk/v7/interfaces/flagstate" - "github.com/launchdarkly/ldcli/internal/dev_server/adapters" "github.com/launchdarkly/ldcli/internal/dev_server/adapters/mocks" "github.com/launchdarkly/ldcli/internal/dev_server/db" "github.com/launchdarkly/ldcli/internal/dev_server/model" @@ -42,10 +41,7 @@ func TestSDKRoutesViaGoSDK(t *testing.T) { ctx = model.SetObserversOnContext(ctx, observers) // Mock the external LD APIs mockController := gomock.NewController(t) - api := mocks.NewMockApi(mockController) - ctx = adapters.WithApi(ctx, api) - sdk := mocks.NewMockSdk(mockController) - ctx = adapters.WithSdk(ctx, sdk) + ctx, api, sdk := mocks.WithMockApiAndSdk(ctx, mockController) api.EXPECT().GetSdkKey(gomock.Any(), projectKey, environmentKey).Return(testSdkKey, nil).AnyTimes() From 1a67530e086de4c6b8b0cf8bb507aef9fa1d4e15 Mon Sep 17 00:00:00 2001 From: Mike Zorn Date: Mon, 8 Jul 2024 09:34:51 -0700 Subject: [PATCH 58/79] fix: add documentation of the build process (#363) Documents the build process for the dev server. Also fixes a go generate command and removes an extraneous comment. --- internal/dev_server/README.md | 4 +++ internal/dev_server/api/server.go | 2 +- .../dev_server/sdk/stream_client_flags.go | 2 +- internal/dev_server/ui/README.md | 34 ++++++------------- 4 files changed, 16 insertions(+), 26 deletions(-) create mode 100644 internal/dev_server/README.md diff --git a/internal/dev_server/README.md b/internal/dev_server/README.md new file mode 100644 index 00000000..c181dd24 --- /dev/null +++ b/internal/dev_server/README.md @@ -0,0 +1,4 @@ +# dev server +The dev server is a go server that ldcli can run. It provides a local-only version of all the APIs that support LaunchDarkly SDKs. You can use it to serve flags to local and ephemeral environments. It copies flag _values_ for a project from a source environment and serves those. There are also APIs that let you override those values so that you can enable a feature just in your dev environment, e.g. + +The build of the dev server is incorporated into the ldcli build itself. The UI provided by the dev server has a [manual build](./ui/README.md). \ No newline at end of file diff --git a/internal/dev_server/api/server.go b/internal/dev_server/api/server.go index cdaf80d9..e7acdc3e 100644 --- a/internal/dev_server/api/server.go +++ b/internal/dev_server/api/server.go @@ -7,7 +7,7 @@ import ( "github.com/launchdarkly/ldcli/internal/dev_server/model" ) -//go:generate oapi-codegen -config oapi-codegen-cfg.yaml api.yaml +//go:generate go run github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen -config oapi-codegen-cfg.yaml api.yaml type Server struct { } diff --git a/internal/dev_server/sdk/stream_client_flags.go b/internal/dev_server/sdk/stream_client_flags.go index e5fd953c..905a4c61 100644 --- a/internal/dev_server/sdk/stream_client_flags.go +++ b/internal/dev_server/sdk/stream_client_flags.go @@ -22,7 +22,7 @@ func StreamClientFlags(w http.ResponseWriter, r *http.Request) { WriteError(ctx, w, errors.Wrap(err, "failed to marshal flag state")) return } - updateChan, doneChan := OpenStream(w, r.Context().Done(), Message{"put", jsonBody}) // TODO Wireup updateChan + updateChan, doneChan := OpenStream(w, r.Context().Done(), Message{"put", jsonBody}) defer close(updateChan) projectKey := GetProjectKeyFromContext(ctx) observer := clientFlagsObserver{updateChan, projectKey} diff --git a/internal/dev_server/ui/README.md b/internal/dev_server/ui/README.md index 9d0b4bcd..fc740b32 100644 --- a/internal/dev_server/ui/README.md +++ b/internal/dev_server/ui/README.md @@ -1,30 +1,16 @@ -# React + TypeScript + Vite +# Dev Server UI -This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. +The dev server UI is a very small react app that is used to view the flags & flag variations that the dev server will serve. -Currently, two official plugins are available: +**NOTE: be sure to run all commands in the `internal/ui` directory** -- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh -- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh +## Prerequisites +This uses `vite` for builds & `npm` to manage dependencies. `npm i` to install dependencies. -## Expanding the ESLint configuration +## Build +Because the UI is modified far less frequently than the rest of the CLI, we build the UI locally so that we don't need the JS toolchain in CI. -If you are developing a production application, we recommend updating the configuration to enable type aware lint rules: +To update the UI that is served by the dev sever, run `npm run build` and rebuild the go server. Be sure to check in the update in `dist` so that your changes are incorporated into the go build -- Configure the top-level `parserOptions` property like this: - -```js -export default { - // other rules... - parserOptions: { - ecmaVersion: 'latest', - sourceType: 'module', - project: ['./tsconfig.json', './tsconfig.node.json'], - tsconfigRootDir: __dirname, - }, -}; -``` - -- Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked` -- Optionally add `plugin:@typescript-eslint/stylistic-type-checked` -- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list +## Run +When developing, you can use `npm run dev` to spin up a development version of the UI. Be sure to also run the dev server to provide the APIs. From 37dddb611caa73c95e05d915d64812f889d139be Mon Sep 17 00:00:00 2001 From: Kelly Hofmann <55991524+k3llymariee@users.noreply.github.com> Date: Mon, 8 Jul 2024 10:13:07 -0700 Subject: [PATCH 59/79] use resource client instead of separate dev server client (#369) * chore: Replace login client with resources client (#366) Replace login client with resources client * Ensure extra trailing slash doesn't break requests * use new unauthenticated request method on resources client * fix imports --------- Co-authored-by: Danny Olson --- cmd/dev_server/dev_server.go | 17 ++--- cmd/dev_server/overrides.go | 14 ++-- cmd/dev_server/projects.go | 32 ++++----- cmd/flags/archive.go | 6 +- cmd/flags/toggle.go | 5 +- cmd/login/login.go | 11 +++- cmd/members/invite.go | 5 +- cmd/resources/resources.go | 1 + cmd/root.go | 7 +- internal/analytics/client.go | 7 +- internal/config/config_service.go | 7 +- internal/dev_server/client.go | 45 ------------- internal/errors/errors.go | 5 +- internal/login/client.go | 69 -------------------- internal/login/login.go | 11 ++-- internal/login/login_test.go | 11 ++-- internal/resources/client.go | 43 ++++++++++-- internal/{login => resources}/client_test.go | 19 +++--- internal/resources/mock_client.go | 14 ++++ 19 files changed, 139 insertions(+), 190 deletions(-) delete mode 100644 internal/dev_server/client.go delete mode 100644 internal/login/client.go rename internal/{login => resources}/client_test.go (73%) diff --git a/cmd/dev_server/dev_server.go b/cmd/dev_server/dev_server.go index 58316457..c2269cd8 100644 --- a/cmd/dev_server/dev_server.go +++ b/cmd/dev_server/dev_server.go @@ -7,9 +7,10 @@ import ( "github.com/launchdarkly/ldcli/cmd/cliflags" resourcecmd "github.com/launchdarkly/ldcli/cmd/resources" "github.com/launchdarkly/ldcli/internal/dev_server" + "github.com/launchdarkly/ldcli/internal/resources" ) -func NewDevServerCmd(localClient dev_server.LocalClient, ldClient dev_server.Client) *cobra.Command { +func NewDevServerCmd(client resources.Client, ldClient dev_server.Client) *cobra.Command { cmd := &cobra.Command{ Use: "dev-server", Short: "Development server", @@ -25,15 +26,15 @@ func NewDevServerCmd(localClient dev_server.LocalClient, ldClient dev_server.Cli // Add subcommands here cmd.AddGroup(&cobra.Group{ID: "projects", Title: "Project commands:"}) - cmd.AddCommand(NewListProjectsCmd(localClient)) - cmd.AddCommand(NewGetProjectCmd(localClient)) - cmd.AddCommand(NewSyncProjectCmd(localClient)) - cmd.AddCommand(NewRemoveProjectCmd(localClient)) - cmd.AddCommand(NewAddProjectCmd(localClient)) + cmd.AddCommand(NewListProjectsCmd(client)) + cmd.AddCommand(NewGetProjectCmd(client)) + cmd.AddCommand(NewSyncProjectCmd(client)) + cmd.AddCommand(NewRemoveProjectCmd(client)) + cmd.AddCommand(NewAddProjectCmd(client)) cmd.AddGroup(&cobra.Group{ID: "overrides", Title: "Override commands:"}) - cmd.AddCommand(NewAddOverrideCmd(localClient)) - cmd.AddCommand(NewRemoveOverrideCmd(localClient)) + cmd.AddCommand(NewAddOverrideCmd(client)) + cmd.AddCommand(NewRemoveOverrideCmd(client)) cmd.AddGroup(&cobra.Group{ID: "server", Title: "Server commands:"}) cmd.AddCommand(NewStartServerCmd(ldClient)) diff --git a/cmd/dev_server/overrides.go b/cmd/dev_server/overrides.go index f834440c..fd9d6996 100644 --- a/cmd/dev_server/overrides.go +++ b/cmd/dev_server/overrides.go @@ -10,11 +10,11 @@ import ( "github.com/launchdarkly/ldcli/cmd/cliflags" resourcescmd "github.com/launchdarkly/ldcli/cmd/resources" "github.com/launchdarkly/ldcli/cmd/validators" - "github.com/launchdarkly/ldcli/internal/dev_server" "github.com/launchdarkly/ldcli/internal/output" + "github.com/launchdarkly/ldcli/internal/resources" ) -func NewAddOverrideCmd(client dev_server.LocalClient) *cobra.Command { +func NewAddOverrideCmd(client resources.Client) *cobra.Command { cmd := &cobra.Command{ GroupID: "overrides", Args: validators.Validate(), @@ -44,7 +44,7 @@ func NewAddOverrideCmd(client dev_server.LocalClient) *cobra.Command { return cmd } -func addOverride(client dev_server.LocalClient) func(*cobra.Command, []string) error { +func addOverride(client resources.Client) func(*cobra.Command, []string) error { return func(cmd *cobra.Command, args []string) error { var data interface{} err := json.Unmarshal([]byte(viper.GetString(cliflags.DataFlag)), &data) @@ -58,7 +58,7 @@ func addOverride(client dev_server.LocalClient) func(*cobra.Command, []string) e } path := fmt.Sprintf("%s/dev/projects/%s/overrides/%s", DEV_SERVER, viper.GetString(cliflags.ProjectFlag), viper.GetString(cliflags.FlagFlag)) - res, err := client.MakeRequest( + res, err := client.MakeUnauthenticatedRequest( "PUT", path, jsonData, @@ -73,7 +73,7 @@ func addOverride(client dev_server.LocalClient) func(*cobra.Command, []string) e } } -func NewRemoveOverrideCmd(client dev_server.LocalClient) *cobra.Command { +func NewRemoveOverrideCmd(client resources.Client) *cobra.Command { cmd := &cobra.Command{ GroupID: "overrides", Args: validators.Validate(), @@ -98,10 +98,10 @@ func NewRemoveOverrideCmd(client dev_server.LocalClient) *cobra.Command { return cmd } -func removeOverride(client dev_server.LocalClient) func(*cobra.Command, []string) error { +func removeOverride(client resources.Client) func(*cobra.Command, []string) error { return func(cmd *cobra.Command, args []string) error { path := fmt.Sprintf("%s/dev/projects/%s/overrides/%s", DEV_SERVER, viper.GetString(cliflags.ProjectFlag), viper.GetString(cliflags.FlagFlag)) - res, err := client.MakeRequest( + res, err := client.MakeUnauthenticatedRequest( "DELETE", path, nil, diff --git a/cmd/dev_server/projects.go b/cmd/dev_server/projects.go index 559a7da0..45e83762 100644 --- a/cmd/dev_server/projects.go +++ b/cmd/dev_server/projects.go @@ -10,13 +10,13 @@ import ( "github.com/launchdarkly/ldcli/cmd/cliflags" resourcescmd "github.com/launchdarkly/ldcli/cmd/resources" "github.com/launchdarkly/ldcli/cmd/validators" - "github.com/launchdarkly/ldcli/internal/dev_server" "github.com/launchdarkly/ldcli/internal/output" + "github.com/launchdarkly/ldcli/internal/resources" ) const DEV_SERVER = "http://0.0.0.0:8765" -func NewListProjectsCmd(client dev_server.LocalClient) *cobra.Command { +func NewListProjectsCmd(client resources.Client) *cobra.Command { cmd := &cobra.Command{ GroupID: "projects", Args: validators.Validate(), @@ -31,11 +31,11 @@ func NewListProjectsCmd(client dev_server.LocalClient) *cobra.Command { return cmd } -func listProjects(client dev_server.LocalClient) func(*cobra.Command, []string) error { +func listProjects(client resources.Client) func(*cobra.Command, []string) error { return func(cmd *cobra.Command, args []string) error { path := DEV_SERVER + "/dev/projects" - res, err := client.MakeRequest( + res, err := client.MakeUnauthenticatedRequest( "GET", path, nil, @@ -50,7 +50,7 @@ func listProjects(client dev_server.LocalClient) func(*cobra.Command, []string) } } -func NewGetProjectCmd(client dev_server.LocalClient) *cobra.Command { +func NewGetProjectCmd(client resources.Client) *cobra.Command { cmd := &cobra.Command{ GroupID: "projects", Args: validators.Validate(), @@ -70,11 +70,11 @@ func NewGetProjectCmd(client dev_server.LocalClient) *cobra.Command { return cmd } -func getProject(client dev_server.LocalClient) func(*cobra.Command, []string) error { +func getProject(client resources.Client) func(*cobra.Command, []string) error { return func(cmd *cobra.Command, args []string) error { path := DEV_SERVER + "/dev/projects/" + viper.GetString(cliflags.ProjectFlag) - res, err := client.MakeRequest( + res, err := client.MakeUnauthenticatedRequest( "GET", path, nil, @@ -88,7 +88,7 @@ func getProject(client dev_server.LocalClient) func(*cobra.Command, []string) er } } -func NewSyncProjectCmd(client dev_server.LocalClient) *cobra.Command { +func NewSyncProjectCmd(client resources.Client) *cobra.Command { cmd := &cobra.Command{ GroupID: "projects", Args: validators.Validate(), @@ -108,11 +108,11 @@ func NewSyncProjectCmd(client dev_server.LocalClient) *cobra.Command { return cmd } -func syncProject(client dev_server.LocalClient) func(*cobra.Command, []string) error { +func syncProject(client resources.Client) func(*cobra.Command, []string) error { return func(cmd *cobra.Command, args []string) error { path := DEV_SERVER + "/dev/projects/" + viper.GetString(cliflags.ProjectFlag) + "/sync" - res, err := client.MakeRequest( + res, err := client.MakeUnauthenticatedRequest( "PATCH", path, nil, @@ -126,7 +126,7 @@ func syncProject(client dev_server.LocalClient) func(*cobra.Command, []string) e } } -func NewRemoveProjectCmd(client dev_server.LocalClient) *cobra.Command { +func NewRemoveProjectCmd(client resources.Client) *cobra.Command { cmd := &cobra.Command{ GroupID: "projects", Args: validators.Validate(), @@ -146,11 +146,11 @@ func NewRemoveProjectCmd(client dev_server.LocalClient) *cobra.Command { return cmd } -func deleteProject(client dev_server.LocalClient) func(*cobra.Command, []string) error { +func deleteProject(client resources.Client) func(*cobra.Command, []string) error { return func(cmd *cobra.Command, args []string) error { path := DEV_SERVER + "/dev/projects/" + viper.GetString(cliflags.ProjectFlag) - res, err := client.MakeRequest( + res, err := client.MakeUnauthenticatedRequest( "DELETE", path, nil, @@ -165,7 +165,7 @@ func deleteProject(client dev_server.LocalClient) func(*cobra.Command, []string) } } -func NewAddProjectCmd(client dev_server.LocalClient) *cobra.Command { +func NewAddProjectCmd(client resources.Client) *cobra.Command { cmd := &cobra.Command{ GroupID: "projects", Args: validators.Validate(), @@ -208,7 +208,7 @@ type postBody struct { //context context } -func addProject(client dev_server.LocalClient) func(*cobra.Command, []string) error { +func addProject(client resources.Client) func(*cobra.Command, []string) error { return func(cmd *cobra.Command, args []string) error { body := postBody{SourceEnvironmentKey: viper.GetString("source")} //if viper.IsSet("context-key") { @@ -224,7 +224,7 @@ func addProject(client dev_server.LocalClient) func(*cobra.Command, []string) er } path := DEV_SERVER + "/dev/projects/" + viper.GetString(cliflags.ProjectFlag) - res, err := client.MakeRequest( + res, err := client.MakeUnauthenticatedRequest( "POST", path, jsonData, diff --git a/cmd/flags/archive.go b/cmd/flags/archive.go index 48d79483..ea07fe6c 100644 --- a/cmd/flags/archive.go +++ b/cmd/flags/archive.go @@ -2,6 +2,7 @@ package flags import ( "fmt" + "net/url" "github.com/spf13/cobra" "github.com/spf13/viper" @@ -31,10 +32,9 @@ func NewArchiveCmd(client resources.Client) *cobra.Command { func makeArchiveRequest(client resources.Client) func(*cobra.Command, []string) error { return func(cmd *cobra.Command, args []string) error { - - path := fmt.Sprintf( - "%s/api/v2/flags/%s/%s", + path, _ := url.JoinPath( viper.GetString(cliflags.BaseURIFlag), + "api/v2/flags", viper.GetString(cliflags.ProjectFlag), viper.GetString(cliflags.FlagFlag), ) diff --git a/cmd/flags/toggle.go b/cmd/flags/toggle.go index 926800d4..2b79e33d 100644 --- a/cmd/flags/toggle.go +++ b/cmd/flags/toggle.go @@ -2,6 +2,7 @@ package flags import ( "fmt" + "net/url" "github.com/spf13/cobra" "github.com/spf13/viper" @@ -53,9 +54,9 @@ func runE(client resources.Client) func(*cobra.Command, []string) error { toggleOn = false } - path := fmt.Sprintf( - "%s/api/v2/flags/%s/%s", + path, _ := url.JoinPath( viper.GetString(cliflags.BaseURIFlag), + "api/v2/flags", viper.GetString(cliflags.ProjectFlag), viper.GetString(cliflags.FlagFlag), ) diff --git a/cmd/login/login.go b/cmd/login/login.go index 9a5c0b20..858283a4 100644 --- a/cmd/login/login.go +++ b/cmd/login/login.go @@ -2,6 +2,7 @@ package login import ( "fmt" + "net/url" "os" "strings" @@ -15,11 +16,12 @@ import ( "github.com/launchdarkly/ldcli/internal/analytics" "github.com/launchdarkly/ldcli/internal/config" "github.com/launchdarkly/ldcli/internal/login" + "github.com/launchdarkly/ldcli/internal/resources" ) func NewLoginCmd( analyticsTrackerFn analytics.TrackerFn, - client login.Client, + client resources.UnauthenticatedClient, ) *cobra.Command { cmd := cobra.Command{ Long: "", @@ -55,7 +57,7 @@ func NewLoginCmd( return &cmd } -func run(client login.Client) func(*cobra.Command, []string) error { +func run(client resources.UnauthenticatedClient) func(*cobra.Command, []string) error { return func(cmd *cobra.Command, args []string) error { if ok, err := config.AccessTokenIsSet(viper.GetViper().ConfigFileUsed()); !ok { return err @@ -71,7 +73,10 @@ func run(client login.Client) func(*cobra.Command, []string) error { return err } - fullURL := fmt.Sprintf("%s/%s", viper.GetString(cliflags.BaseURIFlag), deviceAuthorization.VerificationURI) + fullURL, _ := url.JoinPath( + viper.GetString(cliflags.BaseURIFlag), + deviceAuthorization.VerificationURI, + ) var b strings.Builder b.WriteString(fmt.Sprintf("Your code is %s\n", deviceAuthorization.UserCode)) b.WriteString("This code verifies your authentication with LaunchDarkly.\n") diff --git a/cmd/members/invite.go b/cmd/members/invite.go index ae2c6d11..f6ea45ff 100644 --- a/cmd/members/invite.go +++ b/cmd/members/invite.go @@ -3,6 +3,7 @@ package members import ( "encoding/json" "fmt" + "net/url" "github.com/spf13/cobra" "github.com/spf13/viper" @@ -45,9 +46,9 @@ func runE(client resources.Client) func(*cobra.Command, []string) error { return errors.NewError(err.Error()) } - path := fmt.Sprintf( - "%s/api/v2/members", + path, _ := url.JoinPath( viper.GetString(cliflags.BaseURIFlag), + "api/v2/members", ) res, err := client.MakeRequest( viper.GetString(cliflags.AccessTokenFlag), diff --git a/cmd/resources/resources.go b/cmd/resources/resources.go index 6a12584a..5c34da78 100644 --- a/cmd/resources/resources.go +++ b/cmd/resources/resources.go @@ -283,6 +283,7 @@ func (op *OperationCmd) initFlags() error { } func buildURLWithParams(baseURI, path string, urlParams []string) string { + baseURI = strings.TrimSuffix(baseURI, "/") s := make([]interface{}, len(urlParams)) for i, v := range urlParams { s[i] = v diff --git a/cmd/root.go b/cmd/root.go index 83a5cfba..090fa9f9 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -28,7 +28,6 @@ import ( "github.com/launchdarkly/ldcli/internal/environments" errs "github.com/launchdarkly/ldcli/internal/errors" "github.com/launchdarkly/ldcli/internal/flags" - "github.com/launchdarkly/ldcli/internal/login" "github.com/launchdarkly/ldcli/internal/members" "github.com/launchdarkly/ldcli/internal/projects" "github.com/launchdarkly/ldcli/internal/resources" @@ -38,7 +37,6 @@ type APIClients struct { DevClient dev_server.Client EnvironmentsClient environments.Client FlagsClient flags.Client - LocalClient dev_server.LocalClient MembersClient members.Client ProjectsClient projects.Client ResourcesClient resources.Client @@ -193,9 +191,9 @@ func NewRootCommand( configCmd := configcmd.NewConfigCmd(configService, analyticsTrackerFn) cmd.AddCommand(configCmd.Cmd()) cmd.AddCommand(NewQuickStartCmd(analyticsTrackerFn, clients.EnvironmentsClient, clients.FlagsClient)) - cmd.AddCommand(logincmd.NewLoginCmd(analyticsTrackerFn, login.NewClient(version))) + cmd.AddCommand(logincmd.NewLoginCmd(analyticsTrackerFn, resources.NewClient(version))) cmd.AddCommand(resourcecmd.NewResourcesCmd()) - cmd.AddCommand(devcmd.NewDevServerCmd(dev_server.NewLocalClient(), dev_server.NewClient(version))) + cmd.AddCommand(devcmd.NewDevServerCmd(resources.NewClient(version), dev_server.NewClient(version))) resourcecmd.AddAllResourceCmds(cmd, clients.ResourcesClient, analyticsTrackerFn) // add non-generated commands @@ -220,7 +218,6 @@ func Execute(version string) { DevClient: dev_server.NewClient(version), EnvironmentsClient: environments.NewClient(version), FlagsClient: flags.NewClient(version), - LocalClient: dev_server.NewLocalClient(), MembersClient: members.NewClient(version), ProjectsClient: projects.NewClient(version), ResourcesClient: resources.NewClient(version), diff --git a/internal/analytics/client.go b/internal/analytics/client.go index b17bd3a2..67fec092 100644 --- a/internal/analytics/client.go +++ b/internal/analytics/client.go @@ -6,6 +6,7 @@ import ( "fmt" "io" "net/http" + "net/url" "sync" "time" @@ -81,7 +82,11 @@ func (c *Client) sendEvent(eventName string, properties map[string]interface{}) return } - req, err := http.NewRequest("POST", fmt.Sprintf("%s/internal/tracking", c.baseURI), bytes.NewBuffer(body)) + path, _ := url.JoinPath( + c.baseURI, + "internal/tracking", + ) + req, err := http.NewRequest("POST", path, bytes.NewBuffer(body)) if err != nil { //nolint:staticcheck // TODO: log error c.wg.Done() diff --git a/internal/config/config_service.go b/internal/config/config_service.go index 6c188a04..3985b309 100644 --- a/internal/config/config_service.go +++ b/internal/config/config_service.go @@ -1,7 +1,7 @@ package config import ( - "fmt" + "net/url" "github.com/launchdarkly/ldcli/internal/resources" ) @@ -18,10 +18,7 @@ func NewService(client resources.Client) Service { // VerifyAccessToken is true if the given access token is valid to make API requests. func (s Service) VerifyAccessToken(accessToken string, baseURI string) bool { - path := fmt.Sprintf( - "%s/api/v2/account", - baseURI, - ) + path, _ := url.JoinPath(baseURI, "api/v2/account") _, err := s.client.MakeRequest(accessToken, "HEAD", path, "application/json", nil, nil, false) diff --git a/internal/dev_server/client.go b/internal/dev_server/client.go deleted file mode 100644 index 49bb2b28..00000000 --- a/internal/dev_server/client.go +++ /dev/null @@ -1,45 +0,0 @@ -package dev_server - -import ( - "bytes" - "io" - "net/http" - - "github.com/launchdarkly/ldcli/internal/errors" -) - -type LocalClient interface { - MakeRequest(method, path string, data []byte) ([]byte, error) -} - -type DevClient struct{} - -var _ LocalClient = DevClient{} - -func NewLocalClient() DevClient { - return DevClient{} -} - -func (c DevClient) MakeRequest(method, path string, data []byte) ([]byte, error) { - client := http.Client{} - - req, _ := http.NewRequest(method, path, bytes.NewReader(data)) - req.Header.Add("Content-type", "application/json") - - res, err := client.Do(req) - if err != nil { - return nil, err - } - - body, err := io.ReadAll(res.Body) - if err != nil { - return nil, err - } - defer res.Body.Close() - - if res.StatusCode >= 400 { - return body, errors.NewError(string(body)) - } - - return body, nil -} diff --git a/internal/errors/errors.go b/internal/errors/errors.go index 7beb10ba..6b6af061 100644 --- a/internal/errors/errors.go +++ b/internal/errors/errors.go @@ -3,6 +3,7 @@ package errors import ( "encoding/json" "fmt" + "net/url" "github.com/pkg/errors" @@ -117,5 +118,7 @@ func normalizeUnauthorizedJSON() ([]byte, error) { } func AccessTokenInvalidErrMessage(baseURI string) string { - return fmt.Sprintf("Go to %s/settings/authorization to create an access token.", baseURI) + path, _ := url.JoinPath(baseURI, "settings/authorization") + + return fmt.Sprintf("Go to %s to create an access token.", path) } diff --git a/internal/login/client.go b/internal/login/client.go deleted file mode 100644 index 0209fa42..00000000 --- a/internal/login/client.go +++ /dev/null @@ -1,69 +0,0 @@ -package login - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "net/http" - - "github.com/launchdarkly/ldcli/internal/errors" -) - -type UnauthenticatedClient interface { - MakeRequest( - method string, - path string, - data []byte, - ) ([]byte, error) -} - -type Client struct { - cliVersion string -} - -func NewClient(cliVersion string) Client { - return Client{ - cliVersion: cliVersion, - } -} - -func (c Client) MakeRequest( - method string, - path string, - data []byte, -) ([]byte, error) { - client := http.Client{} - req, _ := http.NewRequest(method, path, bytes.NewReader(data)) - req.Header.Add("Content-Type", "application/json") - req.Header.Set("User-Agent", fmt.Sprintf("launchdarkly-cli/v%s", c.cliVersion)) - res, err := client.Do(req) - if err != nil { - return nil, err - } - - body, err := io.ReadAll(res.Body) - if err != nil { - return nil, err - } - defer res.Body.Close() - - if res.StatusCode < http.StatusBadRequest { - return body, nil - } - - if len(body) > 0 { - return body, errors.NewError(string(body)) - } - - switch res.StatusCode { - case http.StatusMethodNotAllowed: - resp, _ := json.Marshal(map[string]string{ - "code": "method_not_allowed", - "message": "method not allowed", - }) - return body, errors.NewError(string(resp)) - default: - return body, errors.NewError(fmt.Sprintf("could not complete the request: %d", res.StatusCode)) - } -} diff --git a/internal/login/login.go b/internal/login/login.go index 2815fd15..3092b71c 100644 --- a/internal/login/login.go +++ b/internal/login/login.go @@ -7,6 +7,7 @@ import ( "time" "github.com/launchdarkly/ldcli/internal/errors" + "github.com/launchdarkly/ldcli/internal/resources" ) const ( @@ -29,7 +30,7 @@ type DeviceAuthorizationToken struct { // FetchDeviceAuthorization makes a request to create a device authorization that will later be // used to set a local access token if the user grants access. func FetchDeviceAuthorization( - client UnauthenticatedClient, + client resources.UnauthenticatedClient, clientID string, deviceName string, baseURI string, @@ -43,7 +44,7 @@ func FetchDeviceAuthorization( clientID, deviceName, ) - res, err := client.MakeRequest("POST", path, []byte(body)) + res, err := client.MakeUnauthenticatedRequest("POST", path, []byte(body)) if err != nil { return DeviceAuthorization{}, err } @@ -61,7 +62,7 @@ func FetchDeviceAuthorization( // verify their request. If the user denies the request or does nothing long enough for this call // to time out, we do not return an access token. func FetchToken( - client UnauthenticatedClient, + client resources.UnauthenticatedClient, deviceCode string, baseURI string, interval time.Duration, @@ -104,7 +105,7 @@ func FetchToken( } func fetchToken( - client UnauthenticatedClient, + client resources.UnauthenticatedClient, deviceCode string, baseURI string, ) (DeviceAuthorizationToken, error) { @@ -112,7 +113,7 @@ func fetchToken( body, _ := json.Marshal(map[string]string{ "deviceCode": deviceCode, }) - res, err := client.MakeRequest("POST", path, body) + res, err := client.MakeUnauthenticatedRequest("POST", path, body) if err != nil { return DeviceAuthorizationToken{}, err } diff --git a/internal/login/login_test.go b/internal/login/login_test.go index a8afc682..63d6b4d1 100644 --- a/internal/login/login_test.go +++ b/internal/login/login_test.go @@ -7,6 +7,7 @@ import ( "github.com/launchdarkly/ldcli/internal/errors" "github.com/launchdarkly/ldcli/internal/login" + "github.com/launchdarkly/ldcli/internal/resources" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" @@ -16,9 +17,9 @@ type mockClient struct { mock.Mock } -var _ login.UnauthenticatedClient = &mockClient{} +var _ resources.UnauthenticatedClient = &mockClient{} -func (c *mockClient) MakeRequest( +func (c *mockClient) MakeUnauthenticatedRequest( method string, path string, data []byte, @@ -32,7 +33,7 @@ func TestFetchDeviceAuthorization(t *testing.T) { baseURI := "http://test.com" mockClient := mockClient{} mockClient.On( - "MakeRequest", + "MakeUnauthenticatedRequest", "POST", "http://test.com/internal/device-authorization", []byte(`{ @@ -75,7 +76,7 @@ func TestFetchToken(t *testing.T) { }) mockClient := mockClient{} mockClient.On( - "MakeRequest", + "MakeUnauthenticatedRequest", "POST", "http://test.com/internal/device-authorization/token", input, @@ -130,7 +131,7 @@ func TestFetchToken_WithError(t *testing.T) { responseErr := errors.NewError(string(output)) mockClient := mockClient{} mockClient.On( - "MakeRequest", + "MakeUnauthenticatedRequest", "POST", "http://test.com/internal/device-authorization/token", input, diff --git a/internal/resources/client.go b/internal/resources/client.go index 4139a452..01a06786 100644 --- a/internal/resources/client.go +++ b/internal/resources/client.go @@ -2,6 +2,7 @@ package resources import ( "bytes" + "encoding/json" "fmt" "io" "net/http" @@ -10,7 +11,16 @@ import ( "github.com/launchdarkly/ldcli/internal/errors" ) +type UnauthenticatedClient interface { + MakeUnauthenticatedRequest( + method string, + path string, + data []byte, + ) ([]byte, error) +} + type Client interface { + UnauthenticatedClient MakeRequest(accessToken, method, path, contentType string, query url.Values, data []byte, isBeta bool) ([]byte, error) } @@ -24,9 +34,21 @@ func NewClient(cliVersion string) ResourcesClient { return ResourcesClient{cliVersion: cliVersion} } -func (c ResourcesClient) MakeRequest(accessToken, method, path, contentType string, query url.Values, data []byte, isBeta bool) ([]byte, error) { - client := http.Client{} +func (c ResourcesClient) MakeUnauthenticatedRequest( + method string, + path string, + data []byte, +) ([]byte, error) { + return c.MakeRequest("", method, path, "application/json", nil, data, false) +} +func (c ResourcesClient) MakeRequest( + accessToken, method, path, contentType string, + query url.Values, + data []byte, + isBeta bool, +) ([]byte, error) { + client := http.Client{} req, _ := http.NewRequest(method, path, bytes.NewReader(data)) req.Header.Add("Authorization", accessToken) req.Header.Add("Content-Type", contentType) @@ -47,9 +69,22 @@ func (c ResourcesClient) MakeRequest(accessToken, method, path, contentType stri } defer res.Body.Close() - if res.StatusCode >= 400 { + if res.StatusCode < http.StatusBadRequest { + return body, nil + } + + if len(body) > 0 { return body, errors.NewError(string(body)) } - return body, nil + switch res.StatusCode { + case http.StatusMethodNotAllowed: + resp, _ := json.Marshal(map[string]string{ + "code": "method_not_allowed", + "message": "method not allowed", + }) + return body, errors.NewError(string(resp)) + default: + return body, errors.NewError(fmt.Sprintf("could not complete the request: %d", res.StatusCode)) + } } diff --git a/internal/login/client_test.go b/internal/resources/client_test.go similarity index 73% rename from internal/login/client_test.go rename to internal/resources/client_test.go index 71450123..7b341adb 100644 --- a/internal/login/client_test.go +++ b/internal/resources/client_test.go @@ -1,22 +1,23 @@ -package login_test +package resources_test import ( "net/http" "net/http/httptest" "testing" - "github.com/launchdarkly/ldcli/internal/login" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/launchdarkly/ldcli/internal/resources" ) -func TestMakeRequest(t *testing.T) { +func TestMakeUnauthenticatedRequest(t *testing.T) { t.Run("with a successful response", func(t *testing.T) { server := makeServer(t, http.StatusOK, `{"message": "success"}`) defer server.Close() - c := login.NewClient("test-version") + c := resources.NewClient("test-version") - response, err := c.MakeRequest("POST", server.URL, []byte(`{}`)) + response, err := c.MakeUnauthenticatedRequest("POST", server.URL, []byte(`{}`)) require.NoError(t, err) assert.JSONEq(t, `{"message": "success"}`, string(response)) @@ -25,9 +26,9 @@ func TestMakeRequest(t *testing.T) { t.Run("with an error with a response body", func(t *testing.T) { server := makeServer(t, http.StatusBadRequest, `{"message": "an error"}`) defer server.Close() - c := login.NewClient("test-version") + c := resources.NewClient("test-version") - _, err := c.MakeRequest("POST", server.URL, []byte(`{}`)) + _, err := c.MakeUnauthenticatedRequest("POST", server.URL, []byte(`{}`)) assert.EqualError(t, err, `{"message": "an error"}`) }) @@ -35,13 +36,13 @@ func TestMakeRequest(t *testing.T) { t.Run("with a method not allowed status code and no body", func(t *testing.T) { server := makeServer(t, http.StatusMethodNotAllowed, "") defer server.Close() - c := login.NewClient("test-version") + c := resources.NewClient("test-version") expectedResponse := `{ "code": "method_not_allowed", "message": "method not allowed" }` - _, err := c.MakeRequest("POST", server.URL, []byte(`{}`)) + _, err := c.MakeUnauthenticatedRequest("POST", server.URL, []byte(`{}`)) assert.JSONEq(t, expectedResponse, err.Error()) }) diff --git a/internal/resources/mock_client.go b/internal/resources/mock_client.go index 85e143df..718b6208 100644 --- a/internal/resources/mock_client.go +++ b/internal/resources/mock_client.go @@ -28,3 +28,17 @@ func (c *MockClient) MakeRequest( return c.Response, nil } + +func (c *MockClient) MakeUnauthenticatedRequest( + method string, + path string, + data []byte, +) ([]byte, error) { + c.Input = data + + if c.StatusCode > http.StatusBadRequest { + return c.Response, c.Err + } + + return c.Response, nil +} From 7c6597eb224bdaf1557332ed638c359aa83883fc Mon Sep 17 00:00:00 2001 From: Thomas Varney <70972175+tvarney13@users.noreply.github.com> Date: Tue, 9 Jul 2024 12:27:32 -0400 Subject: [PATCH 60/79] feat: Allow for the customization of the port when starting dev server (#373) * add params object to allow easier extensibility of function args * don't allow full customization of the IP, just port (duh) * Add cmd param * reorderin' Co-authored-by: Mike Zorn --------- Co-authored-by: Mike Zorn --- cmd/cliflags/flags.go | 4 ++++ cmd/dev_server/dev_server.go | 8 ++++++++ cmd/dev_server/start_server.go | 13 +++++++------ internal/dev_server/dev_server.go | 23 ++++++++++++++++------- 4 files changed, 35 insertions(+), 13 deletions(-) diff --git a/cmd/cliflags/flags.go b/cmd/cliflags/flags.go index 75464f6a..d8902240 100644 --- a/cmd/cliflags/flags.go +++ b/cmd/cliflags/flags.go @@ -3,6 +3,7 @@ package cliflags const ( BaseURIDefault = "https://app.launchdarkly.com" DevStreamURIDefault = "https://stream.launchdarkly.com" + PortDefault = "8765" AccessTokenFlag = "access-token" AnalyticsOptOut = "analytics-opt-out" @@ -13,6 +14,7 @@ const ( EnvironmentFlag = "environment" FlagFlag = "flag" OutputFlag = "output" + PortFlag = "port" ProjectFlag = "project" RoleFlag = "role" @@ -23,6 +25,7 @@ const ( EnvironmentFlagDescription = "Default environment key" FlagFlagDescription = "Default feature flag key" OutputFlagDescription = "Command response output format in either JSON or plain text" + PortFlagDescription = "Port for the dev server to run on" ProjectFlagDescription = "Default project key" ) @@ -35,6 +38,7 @@ func AllFlagsHelp() map[string]string { EnvironmentFlag: EnvironmentFlagDescription, FlagFlag: FlagFlagDescription, OutputFlag: OutputFlagDescription, + PortFlag: PortFlagDescription, ProjectFlag: ProjectFlagDescription, } } diff --git a/cmd/dev_server/dev_server.go b/cmd/dev_server/dev_server.go index c2269cd8..f12b7fdf 100644 --- a/cmd/dev_server/dev_server.go +++ b/cmd/dev_server/dev_server.go @@ -24,6 +24,14 @@ func NewDevServerCmd(client resources.Client, ldClient dev_server.Client) *cobra ) _ = viper.BindPFlag(cliflags.DevStreamURIFlag, cmd.PersistentFlags().Lookup(cliflags.DevStreamURIFlag)) + cmd.PersistentFlags().String( + cliflags.PortFlag, + cliflags.PortDefault, + cliflags.PortFlagDescription, + ) + + _ = viper.BindPFlag(cliflags.PortFlag, cmd.PersistentFlags().Lookup(cliflags.PortFlag)) + // Add subcommands here cmd.AddGroup(&cobra.Group{ID: "projects", Title: "Project commands:"}) cmd.AddCommand(NewListProjectsCmd(client)) diff --git a/cmd/dev_server/start_server.go b/cmd/dev_server/start_server.go index 3ca9d3ee..cd53cbc3 100644 --- a/cmd/dev_server/start_server.go +++ b/cmd/dev_server/start_server.go @@ -34,13 +34,14 @@ func NewStartServerCmd(client dev_server.Client) *cobra.Command { func startServer(client dev_server.Client) func(*cobra.Command, []string) error { return func(cmd *cobra.Command, args []string) error { ctx := context.Background() + params := dev_server.ServerParams{ + AccessToken: viper.GetString(cliflags.AccessTokenFlag), + BaseURI: viper.GetString(cliflags.BaseURIFlag), + DevStreamURI: viper.GetString(cliflags.DevStreamURIFlag), + Port: viper.GetString(cliflags.PortFlag), + } - client.RunServer( - ctx, - viper.GetString(cliflags.AccessTokenFlag), - viper.GetString(cliflags.BaseURIFlag), - viper.GetString(cliflags.DevStreamURIFlag), - ) + client.RunServer(ctx, params) return nil } diff --git a/internal/dev_server/dev_server.go b/internal/dev_server/dev_server.go index 844a5f67..a44926a1 100644 --- a/internal/dev_server/dev_server.go +++ b/internal/dev_server/dev_server.go @@ -20,7 +20,14 @@ import ( ) type Client interface { - RunServer(ctx context.Context, accessToken, baseURI, devStreamURI string) + RunServer(ctx context.Context, serverParams ServerParams) +} + +type ServerParams struct { + AccessToken string + BaseURI string + DevStreamURI string + Port string } type LDClient struct { @@ -33,8 +40,8 @@ func NewClient(cliVersion string) LDClient { return LDClient{cliVersion: cliVersion} } -func (c LDClient) RunServer(ctx context.Context, accessToken, baseURI, devStreamURI string) { - ldClient := client.New(accessToken, baseURI, c.cliVersion) +func (c LDClient) RunServer(ctx context.Context, serverParams ServerParams) { + ldClient := client.New(serverParams.AccessToken, serverParams.BaseURI, c.cliVersion) dbPath := getDBPath() log.Printf("Using database at %s", dbPath) sqlStore, err := db.NewSqlite(ctx, getDBPath()) @@ -47,7 +54,7 @@ func (c LDClient) RunServer(ctx context.Context, accessToken, baseURI, devStream ResponseErrorHandlerFunc: ResponseErrorHandler, }) r := mux.NewRouter() - r.Use(adapters.Middleware(*ldClient, devStreamURI)) + r.Use(adapters.Middleware(*ldClient, serverParams.DevStreamURI)) r.Use(model.StoreMiddleware(sqlStore)) r.Use(model.ObserversMiddleware(model.NewObservers())) r.Handle("/ui", http.RedirectHandler("/ui/", http.StatusMovedPermanently)) @@ -56,10 +63,12 @@ func (c LDClient) RunServer(ctx context.Context, accessToken, baseURI, devStream handler := api.HandlerFromMux(apiServer, r) handler = handlers.CombinedLoggingHandler(os.Stdout, handler) handler = handlers.RecoveryHandler(handlers.PrintRecoveryStack(true))(handler) - fmt.Println("Server running on 0.0.0.0:8765") - fmt.Println("Access the UI for toggling overrides at http://localhost:8765/ui or by running `ldcli dev-server ui`") + + addr := fmt.Sprintf("0.0.0.0:%s", serverParams.Port) + fmt.Printf("Server running on %s", addr) + fmt.Printf("Access the UI for toggling overrides at http://localhost:%s/ui or by running `ldcli dev-server ui`", serverParams.Port) server := http.Server{ - Addr: "0.0.0.0:8765", + Addr: addr, Handler: handler, } log.Fatal(server.ListenAndServe()) From 786c548de954c54b72a08a2d15ee3193e7e56331 Mon Sep 17 00:00:00 2001 From: Thomas Varney <70972175+tvarney13@users.noreply.github.com> Date: Tue, 9 Jul 2024 13:19:47 -0400 Subject: [PATCH 61/79] feat: Sync publishes streaming update (#371) * Add event, emit event, rename a field * server side update * client side event * Code consolidation and cleanup * little more cleanup * half an attempt at an integration test * correct log text * add overrides * add expected times (causes timeout) * make test go * Make comment more accurate Co-authored-by: Mike Zorn * chans are pointer types already * modify test --------- Co-authored-by: Mike Zorn --- internal/dev_server/api/server.go | 8 ++-- internal/dev_server/db/sqlite.go | 6 +-- internal/dev_server/model/events.go | 14 +++++++ internal/dev_server/model/override.go | 13 ++----- internal/dev_server/model/override_test.go | 8 ++-- internal/dev_server/model/project.go | 24 +++++++++--- internal/dev_server/model/project_test.go | 19 ++++++++-- internal/dev_server/sdk/go_sdk_test.go | 37 ++++++++++++++++++- .../dev_server/sdk/stream_client_flags.go | 29 +++++++++++---- .../dev_server/sdk/stream_server_flags.go | 22 ++++++++--- internal/dev_server/sdk/streaming.go | 28 +++++++++++++- 11 files changed, 162 insertions(+), 46 deletions(-) create mode 100644 internal/dev_server/model/events.go diff --git a/internal/dev_server/api/server.go b/internal/dev_server/api/server.go index e7acdc3e..cad380ef 100644 --- a/internal/dev_server/api/server.go +++ b/internal/dev_server/api/server.go @@ -56,7 +56,7 @@ func (s Server) GetDevProjectsProjectKey(ctx context.Context, request GetDevProj LastSyncedFromSource: project.LastSyncTime.Unix(), Context: project.Context, SourceEnvironmentKey: project.SourceEnvironmentKey, - FlagsState: &project.FlagState, + FlagsState: &project.AllFlagsState, } if request.Params.Expand != nil { @@ -98,7 +98,7 @@ func (s Server) PostDevProjectsProjectKey(ctx context.Context, request PostDevPr LastSyncedFromSource: project.LastSyncTime.Unix(), Context: project.Context, SourceEnvironmentKey: project.SourceEnvironmentKey, - FlagsState: &project.FlagState, + FlagsState: &project.AllFlagsState, } if request.Params.Expand != nil { @@ -143,7 +143,7 @@ func (s Server) PatchDevProjectsProjectKey(ctx context.Context, request PatchDev LastSyncedFromSource: project.LastSyncTime.Unix(), Context: project.Context, SourceEnvironmentKey: project.SourceEnvironmentKey, - FlagsState: &project.FlagState, + FlagsState: &project.AllFlagsState, } if request.Params.Expand != nil { @@ -188,7 +188,7 @@ func (s Server) PatchDevProjectsProjectKeySync(ctx context.Context, request Patc LastSyncedFromSource: project.LastSyncTime.Unix(), Context: project.Context, SourceEnvironmentKey: project.SourceEnvironmentKey, - FlagsState: &project.FlagState, + FlagsState: &project.AllFlagsState, } if request.Params.Expand != nil { diff --git a/internal/dev_server/db/sqlite.go b/internal/dev_server/db/sqlite.go index 22063ac6..e833a125 100644 --- a/internal/dev_server/db/sqlite.go +++ b/internal/dev_server/db/sqlite.go @@ -57,7 +57,7 @@ func (s Sqlite) GetDevProject(ctx context.Context, key string) (*model.Project, } // Parse the flag state JSON string - if err := json.Unmarshal([]byte(flagStateData), &project.FlagState); err != nil { + if err := json.Unmarshal([]byte(flagStateData), &project.AllFlagsState); err != nil { return nil, errors.Wrap(err, "unable to unmarshal flag state data") } @@ -65,7 +65,7 @@ func (s Sqlite) GetDevProject(ctx context.Context, key string) (*model.Project, } func (s Sqlite) UpdateProject(ctx context.Context, project model.Project) (bool, error) { - flagsStateJson, err := json.Marshal(project.FlagState) + flagsStateJson, err := json.Marshal(project.AllFlagsState) if err != nil { return false, errors.Wrap(err, "unable to marshal flags state when updating project") } @@ -107,7 +107,7 @@ func (s Sqlite) DeleteDevProject(ctx context.Context, key string) (bool, error) } func (s Sqlite) InsertProject(ctx context.Context, project model.Project) error { - flagsStateJson, err := json.Marshal(project.FlagState) + flagsStateJson, err := json.Marshal(project.AllFlagsState) if err != nil { return errors.Wrap(err, "unable to marshal flags state when writing project") } diff --git a/internal/dev_server/model/events.go b/internal/dev_server/model/events.go new file mode 100644 index 00000000..1701322a --- /dev/null +++ b/internal/dev_server/model/events.go @@ -0,0 +1,14 @@ +package model + +// Event for individual flag overrides +type UpsertOverrideEvent struct { + FlagKey string + ProjectKey string + FlagState FlagState +} + +// Event for full project sync +type SyncEvent struct { + ProjectKey string + AllFlagsState FlagsState +} diff --git a/internal/dev_server/model/override.go b/internal/dev_server/model/override.go index a86f6513..f2eb4918 100644 --- a/internal/dev_server/model/override.go +++ b/internal/dev_server/model/override.go @@ -14,12 +14,6 @@ type Override struct { Version int } -type UpsertOverrideEvent struct { - FlagKey string - ProjectKey string - FlagState FlagState -} - func UpsertOverride(ctx context.Context, projectKey, flagKey string, value ldvalue.Value) (Override, error) { // TODO: validate if the flag type matches @@ -31,7 +25,7 @@ func UpsertOverride(ctx context.Context, projectKey, flagKey string, value ldval } var flagExists bool - for flag := range project.FlagState { + for flag := range project.AllFlagsState { if flagKey == flag { flagExists = true break @@ -54,9 +48,8 @@ func UpsertOverride(ctx context.Context, projectKey, flagKey string, value ldval return Override{}, err } - observers := GetObserversFromContext(ctx) - flagState := override.Apply(project.FlagState[flagKey]) - observers.Notify(UpsertOverrideEvent{ + flagState := override.Apply(project.AllFlagsState[flagKey]) + GetObserversFromContext(ctx).Notify(UpsertOverrideEvent{ FlagKey: flagKey, ProjectKey: projectKey, FlagState: flagState, diff --git a/internal/dev_server/model/override_test.go b/internal/dev_server/model/override_test.go index 0ef4c79b..6b47b563 100644 --- a/internal/dev_server/model/override_test.go +++ b/internal/dev_server/model/override_test.go @@ -28,8 +28,8 @@ func TestUpsertOverride(t *testing.T) { } project := &model.Project{ - Key: projKey, - FlagState: model.FlagsState{flagKey: model.FlagState{Value: ldvalue.Bool(false), Version: 1}}, + Key: projKey, + AllFlagsState: model.FlagsState{flagKey: model.FlagState{Value: ldvalue.Bool(false), Version: 1}}, } ctx = model.ContextWithStore(ctx, store) @@ -50,8 +50,8 @@ func TestUpsertOverride(t *testing.T) { t.Run("Returns error if flag does not exist in project", func(t *testing.T) { badProj := model.Project{ - Key: projKey, - FlagState: model.FlagsState{}, + Key: projKey, + AllFlagsState: model.FlagsState{}, } store.EXPECT().GetDevProject(gomock.Any(), projKey).Return(&badProj, nil) diff --git a/internal/dev_server/model/project.go b/internal/dev_server/model/project.go index 5cc0051c..6d2677cb 100644 --- a/internal/dev_server/model/project.go +++ b/internal/dev_server/model/project.go @@ -14,7 +14,7 @@ type Project struct { SourceEnvironmentKey string Context ldcontext.Context LastSyncTime time.Time - FlagState FlagsState + AllFlagsState FlagsState } // CreateProject creates a project and adds it to the database. @@ -35,7 +35,7 @@ func CreateProject(ctx context.Context, projectKey, sourceEnvironmentKey string, return Project{}, err } - project.FlagState = flagsState + project.AllFlagsState = flagsState project.LastSyncTime = time.Now() store := StoreFromContext(ctx) @@ -65,7 +65,7 @@ func UpdateProject(ctx context.Context, projectKey string, context *ldcontext.Co if err != nil { return Project{}, err } - project.FlagState = flagsState + project.AllFlagsState = flagsState project.LastSyncTime = time.Now() } @@ -76,6 +76,7 @@ func UpdateProject(ctx context.Context, projectKey string, context *ldcontext.Co if !updated { return Project{}, errors.New("Project not updated") } + return *project, nil } @@ -90,8 +91,9 @@ func SyncProject(ctx context.Context, projectKey string) (Project, error) { return Project{}, err } - project.FlagState = flagsState + project.AllFlagsState = flagsState project.LastSyncTime = time.Now() + updated, err := store.UpdateProject(ctx, *project) if err != nil { return Project{}, err @@ -99,6 +101,16 @@ func SyncProject(ctx context.Context, projectKey string) (Project, error) { if !updated { return Project{}, errors.New("Project not updated") } + + allFlagsWithOverrides, err := project.GetFlagStateWithOverridesForProject(ctx) + if err != nil { + return Project{}, errors.Wrapf(err, "unable to get overrides for project, %s", projectKey) + } + + GetObserversFromContext(ctx).Notify(SyncEvent{ + ProjectKey: project.Key, + AllFlagsState: allFlagsWithOverrides, + }) return *project, nil } @@ -108,8 +120,8 @@ func (p Project) GetFlagStateWithOverridesForProject(ctx context.Context) (Flags if err != nil { return FlagsState{}, errors.Wrapf(err, "unable to fetch overrides for project %s", p.Key) } - withOverrides := make(FlagsState, len(p.FlagState)) - for flagKey, flagState := range p.FlagState { + withOverrides := make(FlagsState, len(p.AllFlagsState)) + for flagKey, flagState := range p.AllFlagsState { if override, ok := overrides.GetFlag(flagKey); ok { flagState = override.Apply(flagState) } diff --git a/internal/dev_server/model/project_test.go b/internal/dev_server/model/project_test.go index 48d33b27..2afff534 100644 --- a/internal/dev_server/model/project_test.go +++ b/internal/dev_server/model/project_test.go @@ -58,13 +58,13 @@ func TestCreateProject(t *testing.T) { Key: projKey, SourceEnvironmentKey: sourceEnvKey, Context: ldcontext.NewBuilder("user").Key("dev-environment").Build(), - FlagState: model.FromAllFlags(allFlags), + AllFlagsState: model.FromAllFlags(allFlags), } assert.Equal(t, expectedProj.Key, p.Key) assert.Equal(t, expectedProj.SourceEnvironmentKey, p.SourceEnvironmentKey) assert.Equal(t, expectedProj.Context, p.Context) - assert.Equal(t, expectedProj.FlagState, p.FlagState) + assert.Equal(t, expectedProj.AllFlagsState, p.AllFlagsState) }) } @@ -134,6 +134,11 @@ func TestSyncProject(t *testing.T) { ctx := model.ContextWithStore(context.Background(), store) ctx, api, sdk := adapters_mocks.WithMockApiAndSdk(ctx, mockController) + observer := mocks.NewMockObserver(mockController) + observers := model.NewObservers() + observers.RegisterObserver(observer) + ctx = model.SetObserversOnContext(ctx, observers) + proj := model.Project{ Key: "projKey", SourceEnvironmentKey: "srcEnvKey", @@ -183,6 +188,12 @@ func TestSyncProject(t *testing.T) { api.EXPECT().GetSdkKey(gomock.Any(), proj.Key, proj.SourceEnvironmentKey).Return("sdkKey", nil) sdk.EXPECT().GetAllFlagsState(gomock.Any(), gomock.Any(), "sdkKey").Return(flagstate.AllFlags{}, nil) store.EXPECT().UpdateProject(gomock.Any(), gomock.Any()).Return(true, nil) + observer. + EXPECT(). + Handle(model.SyncEvent{ + ProjectKey: proj.Key, + AllFlagsState: model.FlagsState{}, + }) project, err := model.SyncProject(ctx, proj.Key) assert.Nil(t, err) @@ -196,8 +207,8 @@ func TestGetFlagStateWithOverridesForProject(t *testing.T) { ctx := model.ContextWithStore(context.Background(), store) flagKey := "flg" proj := model.Project{ - Key: "projKey", - FlagState: model.FlagsState{flagKey: model.FlagState{Value: ldvalue.Bool(false), Version: 1}}, + Key: "projKey", + AllFlagsState: model.FlagsState{flagKey: model.FlagState{Value: ldvalue.Bool(false), Version: 1}}, } t.Run("Returns error if store fetch fails", func(t *testing.T) { diff --git a/internal/dev_server/sdk/go_sdk_test.go b/internal/dev_server/sdk/go_sdk_test.go index ac4e5332..b6a32370 100644 --- a/internal/dev_server/sdk/go_sdk_test.go +++ b/internal/dev_server/sdk/go_sdk_test.go @@ -12,6 +12,7 @@ import ( "github.com/launchdarkly/go-sdk-common/v3/ldcontext" "github.com/launchdarkly/go-sdk-common/v3/ldvalue" ldclient "github.com/launchdarkly/go-server-sdk/v7" + "github.com/launchdarkly/go-server-sdk/v7/interfaces" "github.com/launchdarkly/go-server-sdk/v7/interfaces/flagstate" "github.com/launchdarkly/ldcli/internal/dev_server/adapters/mocks" "github.com/launchdarkly/ldcli/internal/dev_server/db" @@ -106,8 +107,42 @@ func TestSDKRoutesViaGoSDK(t *testing.T) { assert.Equal(t, map[string]any{"cat": "hat"}, val.AsArbitraryValue()) }) + // Mock scenario: we re-sync and the SDK returns new values and higher version numbers + updatedFlags := flagstate.NewAllFlagsBuilder(). + AddFlag("boolFlag", flagstate.FlagState{Value: ldvalue.Bool(false), Version: 2}). + AddFlag("stringFlag", flagstate.FlagState{Value: ldvalue.String("pool"), Version: 2}). + AddFlag("intFlag", flagstate.FlagState{Value: ldvalue.Int(789), Version: 2}). + AddFlag("doubleFlag", flagstate.FlagState{Value: ldvalue.Float64(101.01), Version: 2}). + AddFlag("jsonFlag", flagstate.FlagState{Value: ldvalue.CopyArbitraryValue(map[string]any{"cat": "bababooey"}), Version: 2}). + Build() + valuesMap := updatedFlags.ToValuesMap() + + sdk.EXPECT().GetAllFlagsState(gomock.Any(), gomock.Any(), testSdkKey).Return(updatedFlags, nil) + + // This test is testing the "put" payload in a roundabout way by verifying each of the flags are in there. + t.Run("Sync sends full flag payload for project", func(t *testing.T) { + trackers := make(map[string]<-chan interfaces.FlagValueChangeEvent, len(valuesMap)) + + for flagKey := range valuesMap { + flagUpdateChan := ld.GetFlagTracker().AddFlagValueChangeListener(flagKey, ldContext, ldvalue.String("uh-oh")) + defer ld.GetFlagTracker().RemoveFlagValueChangeListener(flagUpdateChan) + trackers[flagKey] = flagUpdateChan + } + + _, err := model.SyncProject(ctx, projectKey) + require.NoError(t, err) + + for flagKey, value := range valuesMap { + updateTracker, ok := trackers[flagKey] + require.True(t, ok) + + update := <-updateTracker + assert.Equal(t, value.AsArbitraryValue(), update.NewValue.AsArbitraryValue()) + } + }) + updates := map[string]ldvalue.Value{ - "boolFlag": ldvalue.Bool(false), + "boolFlag": ldvalue.Bool(true), "stringFlag": ldvalue.String("drool"), "intFlag": ldvalue.Int(456), "doubleFlag": ldvalue.Float64(88.88), diff --git a/internal/dev_server/sdk/stream_client_flags.go b/internal/dev_server/sdk/stream_client_flags.go index 905a4c61..4eac5e91 100644 --- a/internal/dev_server/sdk/stream_client_flags.go +++ b/internal/dev_server/sdk/stream_client_flags.go @@ -22,7 +22,11 @@ func StreamClientFlags(w http.ResponseWriter, r *http.Request) { WriteError(ctx, w, errors.Wrap(err, "failed to marshal flag state")) return } - updateChan, doneChan := OpenStream(w, r.Context().Done(), Message{"put", jsonBody}) + updateChan, doneChan := OpenStream( + w, + r.Context().Done(), + Message{Event: TYPE_PUT, Data: jsonBody}, + ) defer close(updateChan) projectKey := GetProjectKeyFromContext(ctx) observer := clientFlagsObserver{updateChan, projectKey} @@ -50,7 +54,7 @@ func (c clientFlagsObserver) Handle(event interface{}) { log.Printf("clientFlagsObserver: handling flag state event: %v", event) switch event := event.(type) { case model.UpsertOverrideEvent: - data, err := json.Marshal(clientSidePatchData{ + err := SendMessage(c.updateChan, TYPE_PATCH, clientFlag{ Key: event.FlagKey, Version: event.FlagState.Version, Value: event.FlagState.Value, @@ -58,15 +62,26 @@ func (c clientFlagsObserver) Handle(event interface{}) { if err != nil { panic(errors.Wrap(err, "failed to marshal flag state in observer")) } - c.updateChan <- Message{ - Event: "patch", - Data: data, + case model.SyncEvent: + clientFlags := clientFlags{} + for flagKey, flagState := range event.AllFlagsState { + clientFlags[flagKey] = clientFlag{ + Version: flagState.Version, + Value: flagState.Value, + } + } + + err := SendMessage(c.updateChan, TYPE_PUT, clientFlags) + if err != nil { + panic(errors.Wrap(err, "failed to marshal flag state in observer")) } } } -type clientSidePatchData struct { - Key string `json:"key"` +type clientFlag struct { + Key string `json:"key,omitempty"` Version int `json:"version"` Value ldvalue.Value `json:"value"` } + +type clientFlags map[string]clientFlag diff --git a/internal/dev_server/sdk/stream_server_flags.go b/internal/dev_server/sdk/stream_server_flags.go index 45b41613..1d3c0a4f 100644 --- a/internal/dev_server/sdk/stream_server_flags.go +++ b/internal/dev_server/sdk/stream_server_flags.go @@ -24,7 +24,11 @@ func StreamServerAllPayload(w http.ResponseWriter, r *http.Request) { WriteError(ctx, w, errors.Wrap(err, "failed to marshal flag state")) return } - updateChan, doneChan := OpenStream(w, r.Context().Done(), Message{"put", jsonBody}) + updateChan, doneChan := OpenStream( + w, + r.Context().Done(), + Message{Event: TYPE_PUT, Data: jsonBody}, + ) defer close(updateChan) observer := serverFlagsObserver{updateChan, projectKey} observers := model.GetObserversFromContext(ctx) @@ -48,22 +52,28 @@ type serverFlagsObserver struct { } func (c serverFlagsObserver) Handle(event interface{}) { - log.Printf("clientFlagsObserver: handling flag state event: %v", event) + log.Printf("serverFlagsObserver: handling flag state event: %v", event) switch event := event.(type) { case model.UpsertOverrideEvent: if event.ProjectKey != c.projectKey { return } - data, err := json.Marshal(serverSidePatchData{ + + err := SendMessage(c.updateChan, TYPE_PATCH, serverSidePatchData{ Path: fmt.Sprintf("/flags/%s", event.FlagKey), Data: serverFlagFromFlagState(event.FlagKey, event.FlagState), }) if err != nil { panic(errors.Wrap(err, "failed to marshal flag state in observer")) } - c.updateChan <- Message{ - Event: "patch", - Data: data, + case model.SyncEvent: + if event.ProjectKey != c.projectKey { + return + } + + err := SendMessage(c.updateChan, TYPE_PUT, ServerAllPayloadFromFlagsState(event.AllFlagsState)) + if err != nil { + panic(errors.Wrap(err, "failed to marshal flag state in observer")) } } } diff --git a/internal/dev_server/sdk/streaming.go b/internal/dev_server/sdk/streaming.go index 90dba3e5..04f650c8 100644 --- a/internal/dev_server/sdk/streaming.go +++ b/internal/dev_server/sdk/streaming.go @@ -1,6 +1,7 @@ package sdk import ( + "encoding/json" "fmt" "net/http" "time" @@ -8,8 +9,15 @@ import ( "github.com/pkg/errors" ) +type MessageType string + +const ( + TYPE_PUT MessageType = "put" + TYPE_PATCH MessageType = "patch" +) + type Message struct { - Event string + Event MessageType Data []byte } @@ -67,3 +75,21 @@ func OpenStream(w http.ResponseWriter, done <-chan struct{}, initialMessage Mess }() return updateChan, errChan } + +func SendMessage( + updateChan chan<- Message, + msgType MessageType, + data interface{}, +) error { + payload, err := json.Marshal(data) + if err != nil { + return err + } + + updateChan <- Message{ + Event: msgType, + Data: payload, + } + + return nil +} From 6192201877c3843529cc61a3a73798bfb443b5d7 Mon Sep 17 00:00:00 2001 From: Abram Thau <47679905+Glacian22@users.noreply.github.com> Date: Tue, 9 Jul 2024 13:28:09 -0700 Subject: [PATCH 62/79] Abram UI enhancements (#359) * JSON flags now use modal, catch formatting errors * fixed 'only show flags with overrides' bug * refactoring * refactoring * json update modal now dismissed when new override value accepted * minor tweak * remove unnecessarily generated /bin, /.vscode, and undo version change to package-lock * Use close render prop from Dialog --------- Co-authored-by: Abram Thau Co-authored-by: Keegan Watkins --- .vscode/settings.json | 1 - internal/dev_server/ui/README.md | 3 + internal/dev_server/ui/dist/index.html | 40 +++++------ internal/dev_server/ui/src/App.tsx | 98 +++++++++++++++++++------- internal/dev_server/ui/vite.config.ts | 4 +- package-lock.json | 2 +- 6 files changed, 94 insertions(+), 54 deletions(-) delete mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 9e26dfee..00000000 --- a/.vscode/settings.json +++ /dev/null @@ -1 +0,0 @@ -{} \ No newline at end of file diff --git a/internal/dev_server/ui/README.md b/internal/dev_server/ui/README.md index fc740b32..fd7a33ea 100644 --- a/internal/dev_server/ui/README.md +++ b/internal/dev_server/ui/README.md @@ -5,12 +5,15 @@ The dev server UI is a very small react app that is used to view the flags & fla **NOTE: be sure to run all commands in the `internal/ui` directory** ## Prerequisites + This uses `vite` for builds & `npm` to manage dependencies. `npm i` to install dependencies. ## Build + Because the UI is modified far less frequently than the rest of the CLI, we build the UI locally so that we don't need the JS toolchain in CI. To update the UI that is served by the dev sever, run `npm run build` and rebuild the go server. Be sure to check in the update in `dist` so that your changes are incorporated into the go build ## Run + When developing, you can use `npm run dev` to spin up a development version of the UI. Be sure to also run the dev server to provide the APIs. diff --git a/internal/dev_server/ui/dist/index.html b/internal/dev_server/ui/dist/index.html index 7e511b3c..2233693c 100644 --- a/internal/dev_server/ui/dist/index.html +++ b/internal/dev_server/ui/dist/index.html @@ -5,7 +5,7 @@ Vite + React + TS - - diff --git a/internal/dev_server/ui/src/App.tsx b/internal/dev_server/ui/src/App.tsx index 12df0b19..ce1ba773 100644 --- a/internal/dev_server/ui/src/App.tsx +++ b/internal/dev_server/ui/src/App.tsx @@ -5,20 +5,19 @@ import { IconButton, Label, Switch, + Modal, + ModalOverlay, + DialogTrigger, + Dialog, } from '@launchpad-ui/components'; -import { - Box, - CopyToClipboard, - InlineEdit, - TextField, -} from '@launchpad-ui/core'; +import { Box, InlineEdit, TextField } from '@launchpad-ui/core'; import Theme from '@launchpad-ui/tokens'; import './App.css'; -import { useEffect, useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; import { Icon } from '@launchpad-ui/icons'; const API_BASE = import.meta.env.PROD ? '' : '/api'; -const apiRoute = (pathname: string) => `${API_BASE}${pathname}` +const apiRoute = (pathname: string) => `${API_BASE}${pathname}`; function App() { const [flags, setFlags] = useState(null); @@ -28,6 +27,7 @@ function App() { > | null>(null); const [onlyShowOverrides, setOnlyShowOverrides] = useState(false); const overridesPresent = overrides && Object.keys(overrides).length > 0; + const textAreaRef = useRef(null); const updateOverride = (flagKey: string, overrideValue: LDFlagValue) => { fetch(apiRoute(`/dev/projects/default/overrides/${flagKey}`), { @@ -71,6 +71,9 @@ function App() { delete updatedOverrides[flagKey]; setOverrides(updatedOverrides); + + if (Object.keys(updatedOverrides).length === 0) + setOnlyShowOverrides(false); } }) .catch((_e) => { @@ -225,24 +228,67 @@ function App() { break; default: valueNode = ( - { - updateOverride(flagKey, JSON.parse(newValue)); - }} - renderInput={} - > - - {JSON.stringify(hasOverride ? overrideValue : flagValue)} - - + + + + + + {({ close }) => ( +
{ + let newVal; + + try { + newVal = JSON.parse( + textAreaRef?.current?.value || '', + ); + } catch (err) { + window.alert('Invalid JSON formatting'); + return; + } + + updateOverride(flagKey, newVal); + }} + > +